Giter Site home page Giter Site logo

piotrmurach / tty-prompt Goto Github PK

View Code? Open in Web Editor NEW
1.4K 14.0 133.0 1.71 MB

A beautiful and powerful interactive command line prompt

Home Page: https://ttytoolkit.org

License: MIT License

Ruby 100.00%
prompt cli tty tty-components ruby-cli ruby-gem menus terminal terminal-input terminal-menu

tty-prompt's Introduction

TTY Toolkit logo

TTY::Prompt

Gem Version Actions CI Build status Code Climate Coverage Status

A beautiful and powerful interactive command line prompt.

TTY::Prompt provides independent prompt component for 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.

For Windows, consider installing ConEmu, cmder or PowerCmd.

Installation

Add this line to your application's Gemfile:

gem "tty-prompt"

And then execute:

$ bundle

Or install it yourself as:

$ gem install tty-prompt

Contents

1. Usage

In order to start asking questions on the command line, create prompt:

require "tty-prompt"

prompt = TTY::Prompt.new

And then call ask with the question for simple input:

prompt.ask("What is your name?", default: ENV["USER"])
# => What is your name? (piotr)

To confirm input use yes?:

prompt.yes?("Do you like Ruby?")
# => Do you like Ruby? (Y/n)

If you want to input password or secret information use mask:

prompt.mask("What is your secret?")
# => What is your secret? ••••

Asking question with list of options couldn't be easier using select like so:

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:

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:

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:

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:

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:

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:

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 of input is performed. To change this use one of the following conversions:

  • :boolean|:bool - e.g. 'yes/1/y/t/' becomes true, 'no/0/n/f' becomes false
  • :date - parses dates formats "28/03/2020", "March 28th 2020"
  • :time - parses time formats "11:20:03"
  • :float - e.g. -1 becomes -1.0
  • :int|:integer - e.g. +1 becomes 1
  • :sym|:symbol - e.g. "foo" becomes :foo
  • :filepath - converts to file path
  • :path|:pathname - converts to Pathname object
  • :range - e.g. '1-10' becomes 1..10 range object
  • :regexp - e.g. "foo|bar" becomes /foo|bar/
  • :uri - converts to URI object
  • :list|:array - e.g. 'a,b,c' becomes ["a", "b", "c"]
  • :map|:hash - e.g. 'a:1 b:2 c:3' becomes {a: "1", b: "2", c: "3"}

In addition you can specify a plural or append list or array to any base type:

  • :ints or :int_list - will convert to a list of integers
  • :floats or :float_list - will convert to a list of floats
  • :bools or :bool_list - will convert to a list of booleans, e.g. t,f,t becomes [true, false, true]

Similarly, you can append map or hash to any base type:

  • :int_map|:integer_map|:int_hash - will convert to a hash of integers, e.g a:1 b:2 c:3 becomes {a: 1, b: 2, c: 3}
  • :bool_map | :boolean_map|:bool_hash - will convert to a hash of booleans, e.g a:t b:f c:t becomes {a: true, b: false, c: true}

By default, map converts keys to symbols, if you wish to use strings instead specify key type like so:

  • :str_int_map - will convert to a hash of string keys and integer values
  • :string_integer_hash - will convert to a hash of string keys and integer values

For example, if you are interested in range type as answer do the following:

prompt.ask("Provide range of numbers?", convert: :range)
# Provide range of numbers? 1-10
# => 1..10

If, on the other hand, you wish to convert input to a hash of integer values do:

prompt.ask("Provide keys and values:", convert: :int_map)
# Provide keys and values: a=1 b=2 c=3
# => {a: 1, b: 2, c: 3}

If a user provides a wrong type for conversion an error message will be printed in the console:

prompt.ask("Provide digit:", convert: :float)
# Provide digit: x
# >> Cannot convert `x` into 'float' type

You can further customize error message:

prompt.ask("Provide digit:", convert: :float) do |q|
  q.convert(:float, "Wrong value of %{value} for %{type} conversion")
  # or
  q.convert :float
  q.messages[:convert?] = "Wrong value of %{value} for %{type} conversion"
end

You can also provide a custom conversion like so:

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:

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:

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:

prompt.ask("password:", echo: false)

2.1.5 error messages

By default tty-prompt comes with predefined error messages for convert, required, in, validate options.

You can change these and configure to your liking either by passing message as second argument with the option:

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 :convert?, :range?, :required? and :valid?:

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:

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:

prompt.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.

prompt.ask("Enter text:") do |q|
  q.modify :strip, :collapse
end

Available letter casing settings are:

:up         # change to upper case
:down       # change to small case
:capitalize # capitalize each word

Available whitespace settings are:

: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:

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/method.

Validate accepts Regex, Proc or Symbol.

prompt.ask("What is your username?") do |q|
  q.validate(/\A[^.]+\.[^.]+\Z/)
end

The above can also be expressed as a Proc:

prompt.ask("What is your username?") do |q|
  q.validate ->(input) { input =~ /\A[^.]+\.[^.]+\Z/ }
end

There is a built-in validation for :email and you can use it directly like so:

prompt.ask("What is your email?") { |q| q.validate :email }

The default validation message is "Your answer is invalid (must match %{valid})" and you can customise it by passing in a second argument:

prompt.ask("What is your username?") do |q|
  q.validate(/\A[^.]+\.[^.]+\Z/, "Invalid username: %{value}, must match %{valid}")
end

The default message can also be set using messages and the :valid? key:

prompt.ask("What is your username?") do |q|
  q.validate(/\A[^.]+\.[^.]+\Z/)
  q.messages[:valid?] = "Invalid username: %{value}, must match %{valid}")
end

2.2. keypress

In order to ask question that awaits a single character answer use keypress prompt like so:

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:

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:

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:

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.

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:

prompt.multiline("Description?", default: "A super sweet prompt.")

Or using DSL:

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.

prompt.mask("What is your secret?")
# => What is your secret? ••••

The masking character can be changed by passing the :mask key:

heart = prompt.decorate(prompt.symbols[:heart] + " ", :magenta)
prompt.mask("What is your secret?", mask: heart)
# => What is your secret? ❤  ❤  ❤  ❤  ❤

If you don't wish to show any output use :echo option like so:

prompt.mask("What is your secret?", echo: false)

You can also provide validation for your mask to enforce for instance strong passwords:

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:

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:

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:

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:

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:

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:

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:

choices = {small: 1, medium: 2, large: 3}
prompt.select("What size?", choices)
# =>
# What size? (Press ↑/↓ arrow to move and Enter to select)
# ‣ small
#   medium
#   large

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:

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:

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
# =>
# What size? (Press ↑/↓ arrow to move and Enter to select)
# ‣ small
# ✘ medium (out of stock)
#   large

or in a more compact way:

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:

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:

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:

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:

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:

choices = {"Scorpion" => 1, "Kano" => 2, "Jax" => 3}
prompt.select("Choose your destiny?", choices)

To mark particular answer as selected use default with either an index of the choice starting from 1 or a choice's name:

prompt.select("Choose your destiny?") do |menu|
  menu.default 3
  # or menu.default "Jax"

  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

2.6.2.1 :cycle

You can navigate the choices using the arrow keys or define your own key mappings (see 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:

prompt.select("Choose your destiny?", %w(Scorpion Kano Jax), cycle: true)
# =>
# Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select)
# ‣ Scorpion
#   Kano
#   Jax

2.6.2.2 :enum

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.

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

2.6.2.3 :help

You can configure help message with :help and when to display it with :show_help options. The help can be displayed on start, never or always:

choices = %w(Scorpion Kano Jax)
prompt.select("Choose your destiny?", choices, help: "(Bash keyboard keys)", show_help: :always)
# =>
# Choose your destiny? (Bash keyboard keys)
# > Scorpion
#   Kano
#   Jax

2.6.2.4 :marker

You can configure active marker like so:

choices = %w(Scorpion Kano Jax)
prompt.select("Choose your destiny?", choices, symbols: { marker: ">" })
# =>
# Choose your destiny? (Use ↑/↓ and ←/→ arrow keys, press Enter to select)
# > Scorpion
#   Kano
#   Jax

2.6.2.5 :per_page

By default the menu is paginated if selection grows beyond 6 items. To change this setting use :per_page configuration.

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:

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.6 :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:

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:

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.7 :filter

To activate dynamic list searching on letter/number key presses use :filter option:

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":

# =>
# Choose your destiny? (Filter: "k")
# ‣ Kano
#   Kitana

After the user presses "ka":

# =>
# 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:

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:

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:

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 either index(s) of the choice(s) starting from 1 or choice name(s):

prompt.multi_select("Select drinks?") do |menu|
  menu.default 2, 5
  # or menu.default :beer, :whisky

  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

2.6.3.1 :cycle

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:

prompt.multi_select("Select drinks?", %w(vodka beer wine), cycle: true)

2.6.3.2 :enum

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.

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:

# Select drinks? beer, bourbon
# => [{score: 20}, {score: 50}]

2.6.3.3 :help

You can configure help message with :help and when to display it with :show_help options. The help can be displayed on start, never or always:

choices = {vodka: 1, beer: 2, wine: 3, whisky: 4, bourbon: 5}
prompt.multi_select("Select drinks?", choices, help: "Press beer can against keyboard", show_help: :always)
# =>
# Select drinks? (Press beer can against keyboard)"
# ‣ ⬡ vodka
#   ⬡ beer
#   ⬡ wine
#   ⬡ whisky
#   ⬡ bourbon

2.6.3.4 :per_page

By default the menu is paginated if selection grows beyond 6 items. To change this setting use :per_page configuration.

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.5 :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:

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:

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.6 :echo

To control whether the selected items are shown on the question header use the :echo option:

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.7 :filter

To activate dynamic list filtering on letter/number typing, use the :filter option:

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":

# 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.8 :min

To force the minimum number of choices an user must select, use the :min option:

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.9 :max

To limit the number of choices an user can select, use the :max option:

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:

choices = %w(emacs nano vim)
prompt.enum_select("Select an editor?", choices)
# =>
#
# 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:

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 formatting by passing enum option. The default option lets you specify which choice to mark as selected by default. It accepts an index of the choice starting from 1 or a choice name:

choices = %w(nano vim emacs)
prompt.enum_select("Select an editor?") do |menu|
  menu.default 2
  # or menu.defualt "/usr/bin/vim"
  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.

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:

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:

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.

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:

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.

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:

# 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:

# 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:

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.

For example to gather some contact information do:

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:

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:

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.

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'd rather not display all possible values in a vertical list, you may consider using slider. The slider provides easy visual way of picking a value marked by symbol.

For integers, you can set :min(defaults to 0), :max and :step(defaults to 1) options to configure slider range:

prompt.slider("Volume", min: 0, max: 100, step: 5)
# =>
# Volume ──────────●────────── 50
# (Use ←/→ arrow keys, press Enter to select)

For everything else, you can provide an array of your desired choices:

prompt.slider("Letter", ('a'..'z').to_a)
# =>
# Letter ────────────●───────────── m
# (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:

prompt.slider("Volume", max: 100, step: 5, default: 75)
# =>
# Volume ───────────────●────── 75
# (Use ←/→ arrow keys, press Enter to select)

You can also select the default value by name:

prompt.slider("Letter", ('a'..'z').to_a, default: 'q')
# =>
# Letter ──────────────────●─────── q
# (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:

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:

prompt.slider("Volume", max: 10, step: 0.5, default: 5, format: "|:slider| %.1f")
# =>
# Volume |───────────────●──────| 7.5
# (Use ←/→ arrow keys, press Enter to select)

You can alternatively provide a proc/lambda to customize your formatting even further:

slider_format = -> (slider, value) { "|#{slider}| #{value.zero? ? "muted" : "%.1f"}" % value }
prompt.slider("Volume", max: 10, step: 0.5, default: 0, format: slider_format)
# =>
# Volume |●─────────────────────| muted
# (Use ←/→ arrow keys, press Enter to select)

If you wish to change the slider handle and the slider range display use :symbols option:

prompt.slider("Volume", max: 100, step: 5, default: 75, symbols: {bullet: "x", line: "_"})
# =>
# Volume _______________x______ 75%
# (Use ←/→ arrow keys, press Enter to select)

You can configure help message with :help and when to display with :show_help options. The help can be displayed on start, never or always:

prompt.slider("Volume", max: 10, default: 7, help: "(Move arrows left and right to set value)", show_help: :always)
# =>
# Volume ───────────────●────── 7
# (Move arrows left and right to set value)

Slider can be configured through DSL as well:

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)
prompt.slider("What letter?") do |range|
  range.choices ('a'..'z').to_a
  range.format "|:slider| %s"
  range.default 'q'
end
# =>
# What letter? |──────────────────●───────| q
# (Use ←/→ arrow keys, press Enter to select)

2.11 say

To simply print message out to standard output use say like so:

prompt.say(...)

The say method also accepts option :color which supports all the colors provided by pastel

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:

prompt.ok(...)

2.12.2 warn

Print message(s) in yellow do:

prompt.warn(...)

2.11.3 error

Print message(s) in red do:

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.

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:

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:

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:

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. By default it's set to :green value.

The select, multi_select, enum_select and expand prompts use the active color to highlight the currently selected choice.

The answer provided by the user is also highlighted with the active color.

This :active_color as value accepts either a color symbol or callable object.

For example, to change all prompts active color to :cyan do:

prompt = TTY::Prompt.new(active_color: :cyan)

You could also use pastel:

notice = Pastel.new.cyan.on_blue.detach
prompt = TTY::Prompt.new(active_color: notice)

Or use coloring of your own choice:

prompt = TTY::Prompt.new(active_color: ->(str) { my-color-gem(str) })

This option can be applied either globally for all prompts or individually:

prompt.select("What size?", %w(Large Medium Small), active_color: :cyan)

Please see pastel for all supported colors.

3.3 :enable_color

If you wish to disable coloring for a prompt simply pass :enable_color option

prompt = TTY::Prompt.new(enable_color: false)

3.4 :help_color

The :help_color option is used to customize the display color for all the help text. By default it's set to :bright_black value.

Prompts such as select, multi_select, expand support :help_color. This option can be applied either globally for all prompts or individually.

The :help_color option as value accepts either a color symbol or callable object.

For example, to change all prompts help color to :cyan do:

prompt = TTY::Prompt.new(help_color: :cyan)

You could also use pastel:

notice = Pastel.new.cyan.on_blue.detach
prompt = TTY::Prompt.new(help_color: notice)

Or use coloring of your own choice:

prompt = TTY::Prompt.new(help_color: ->(str) { my-color-gem(str) })

Or configure :help_color for an individual prompt:

prompt.select("What size?", %w(Large Medium Small), help_color: :cyan)

Please see pastel for all supported colors.

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 - exits with status code
  • :noop - skips handler
  • custom proc

For example, to send interrupt signal do:

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:

prompt = TTY::Prompt.new(prefix: "[?] ")

3.7 :quiet

Prompts such as select, multi_select, expand, slider support :quiet which is used to disable re-echoing of the question and answer after selection is done. This option can be applied either globally for all prompts or individually.

# global
prompt = TTY::Prompt.new(quiet: true)
# single prompt
prompt.select("What is your favorite color?", %w(blue yellow orange))

3.8 :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:

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 code of conduct.

Copyright

Copyright (c) 2015 Piotr Murach. See LICENSE for further details.

tty-prompt's People

Contributors

2called-chaos avatar 64kramsystem avatar ab avatar ajgon avatar austb avatar biow0lf avatar brodygov avatar burgestrand avatar caalberts avatar carlosefonseca avatar d4be4st avatar dadleyy avatar danielvartanov avatar denisdefreyne avatar frankschmitt avatar hellola avatar ioquatix avatar javierav avatar ktgeek avatar kvs avatar kylekyle avatar mockdeep avatar ondra-m avatar philippeperret avatar piotrmurach avatar pjvleeuwen avatar rafacv avatar rpbaltazar avatar slowbro avatar tylrd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

tty-prompt's Issues

Support list string matching

It would be very useful to support string matching in lists - on user alphanumeric keypress, the entries would update to show only the ones matching the pattern provided (essentially, what the GitHub web interface does when you click on a menu like "Labels" and type something).

(Originally posted, by mistake, on issue #1).

per_page option issue

Very thanks for adding per_page option that is what i exactly want. :)

But only one issue is question title is reproduced each time arrow key is pressed
when elements are less than per_page value. <- I am sure only this case happen or not.

ctrl-c in pry

tty-prompt in causes pry to exit on ctrl-c. It makes it really hard to play in pry with any app that uses tty-prompt.

Use j/k to navigate select lists

Using VIM-like keymaps for most other applications, it would be nice if the select offered a way to configure the up/down keys.

List navigation breaks down for long wrapped items

I'm not sure this can be easily resolved, but I'll try and explain the issue nonetheless. Also, please feel free to change the assignment between tty-prompt and tty-cursor if you wish.

Steps to reproduce:

Navigate a list (e.g. examples/select.rb), which has at lease one item, that wraps around under the current terminal width. Either add a very long item or resize the terminal to be "thin" enough.

Expected behaviour

Normal list navigation

Actual behaviour

The wrapped line now occupies two terminal rows instead of just one; the #clear_line from TTY::Cursor only eats one of them, leaving an artifact when the selection is changed (and #refresh is called accordingly). For example:

require_relative "../lib/tty-prompt"
prompt = TTY::Prompt.new

warriors = [
  "lkasjdlk ajsdl akjsdlkasj dlkasjd lkasjd lkasjd lkasjd lkasjd lkas",
  "askdj alsdkja slkdj aslkdjasl djlasjkd",
  "askdj slkdj aslkdjasl djlasjkd",
  "askdj alsdkja slkdj djlasjkd"
]

prompt.select('Choose your destiny?', warriors)

produces the following output on a terminal with 58 columns:

$ ruby examples/select.rb
Choose your destiny? (Use arrow keys, press Enter to select)
Choose your destiny?
Choose your destiny?
Choose your destiny?
Choose your destiny?
  lkasjdlk ajsdl akjsdlkasj dlkasjd lkasjd lkasjd lkasjd l
kasjd lkas
  askdj alsdkja slkdj aslkdjasl djlasjkd
‣ askdj slkdj aslkdjasl djlasjkd
  askdj alsdkja slkdj djlasjkd

I'm not sure if TTY::Cursor#clear_line should be made aware of wrapped lines, of if the list should be in charge of tracking long lines and calling #clear_line twice for these. Any thoughts?

The markers for select and multi_select are broken

Hi,

On my system, the markers for select and multi_select are broken:
image

My system:

$ lsb_release -a
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.3 LTS
Release:        16.04
Codename:       xenial

$ ruby -v
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]

I noticed I am able to override the marker with both list and multi_list, but I also noticed that the radio markers are hardcoded.

Finally - I ran a test to see all the symbols in your Symbols module, to see what is broken in my system, this is the result, in case it helps.

image

Happy to provide any additional info, or test any fix candidate.

Prompt select , per_page , hash issue

hi

tty-prompt 0.11.0

choices = {'Scorpion' => 1, 'Kano' => 2, 'Jax' => 3}
prompt.select("Choose your letter?", choices)

produce this output:

Choose your letter? (Use arrow keys, press Enter to select)
‣ Scorpion
  Kano
  Jax

Now I set per_page

choices = {'Scorpion' => 1, 'Kano' => 2, 'Jax' => 3}
prompt.select("Choose your letter?", choices, per_page: 2)

the per_page can be 2 , 10 , 25 , always same result

Choose your letter? (Use arrow keys, press Enter to select)
‣ ["Scorpion", 1]

Only one entry is display and baddly

Not clearing the previous lines when switching between select choices on Mac

OS: Mac v10.11.5 (El Capitan)
Terminal: iTerm and Terminal
$TERM: xterm-256color
Ruby: 2.3.1p122

[~/Projects/bug-report]$ bash --noprofile # making sure I dont have any funky settings
bash-3.2$ ruby test.rb
What is your choice (Use arrow keys, press Enter to select)
What is your choice
What is your choice
What is your choice First
Selected: First.

I switch between the choices using up and down arrow, and the line keeps repeating. I assume this is more of an error in TTY::Cursor, rather than TTY::Prompt, but I'm not sure where you want bug reports.

Source code:

require "bundler/setup"
require "tty-prompt"

prompt = TTY::Prompt.new

selected = prompt.select "What is your choice" do |menu|
  menu.choice "First"
  menu.choice "Second"
end

puts "Selected: #{selected}."

Versions of things:

GEM
  remote: https://rubygems.org/
  specs:
    equatable (0.5.0)
    necromancer (0.3.0)
    pastel (0.6.1)
      equatable (~> 0.5.0)
      tty-color (~> 0.3.0)
    tty-color (0.3.0)
    tty-cursor (0.3.0)
    tty-platform (0.1.0)
    tty-prompt (0.6.0)
      necromancer (~> 0.3.0)
      pastel (~> 0.6.0)
      tty-cursor (~> 0.3.0)
      tty-platform (~> 0.1.0)
      wisper (~> 1.6.1)
    wisper (1.6.1)

PLATFORMS
  ruby

DEPENDENCIES
  tty-prompt

BUNDLED WITH
   1.12.5

Emprovement

Hi,

When I code this question,

prompt.ask('Folder name?') do |q|
q.required true
q.validate ->(v) { return !Dir.exist?(v) }
q.messages[:valid?] = 'Folder already exists!'
end

If I press directly [return], the required validation is not used ... and the validate lamba is directly processing.

To correct it , I have to write
prompt.ask('Folder name?') do |q|
# q.required true
q.validate ->(v) { return !v.nil? && !Dir.exist?(v) }
q.messages[:valid?] = 'Folder already exists!'
end

It would be great if the required method was taking account.

Add confirmation input

The input would prompt user for the same input twice. It would automatically validate matching input.

No-echo #ask adds to history

It feels like #ask with the 'echo: false' flag should behave in the same fashion as the #mask method -- not add to history.

Because it does, on the next #ask, you can simply up-arrow to see what was previously hidden.

Reproduction steps:

prompt = TTY::Prompt.new
prompt.ask( "Say something sensitive!", echo: false )
prompt.ask( "Okay, now say something public.  Or up arrow for some juicy secrets!" )

README.md lies about #no? behavior

README.md states:

prompt.no?('Do you hate Ruby?')
# =>
# Do you hate Ruby? (y/N)

What really happends:

hates_ruby = prompt.no?('Do you hate Ruby?')
# =>
# Do you hate Ruby? (Y/n) <--- shows another default 

Also I've failed to find a way to make .ask(..., convert: :bool) to show (y/N), so perhaps it is an issue with it (or with README.md)

Center text?

Just asking if there is a way to center the text in the terminal

NameError: uninitialized constant TTY::Prompt::Utils

Gemfile

gem 'tty-prompt'

Code

require 'tty/prompt'
prompt = TTY::Prompt.new

Error

NameError: uninitialized constant TTY::Prompt::Utils
/home/samuel/.rvm/gems/ruby-2.3.2/gems/tty-prompt-0.10.0/lib/tty/prompt.rb:69:in `initialize'

Yes/No with default = false has wrong capitalisation

The docs have this example for yes?/no?:

prompt.yes?("Are you a human?") do |q|
  q.default false
  q.positive 'Yup'
  q.negative 'Nope'
end
# =>
# Are you a human? (yup/Nope)

So, from this code I expected this output:

prompt = TTY::Prompt.new
prompt.yes?("Are you a human?") do |q|
  q.default false
end
# =>
# Are you a human? (y/N)

But what I'm getting is:

# Are you a human? (Y/n)

I think this is a bug but, since the default parameter is not explained in the yes?/no? docs, I might be misinterpreting it.

Make cycle a Prompt setting

The cycle setting that was added on #66 could be added as a setting on the Prompt instance, and with that, only set it once to retain the old wrap around behaviour on every call without having to add the argument on every one.

track history doesn't run?

Hi Piotr!

thanks for your set of beautiful gems!

I'm trying to use your gem to a smart cli console (see: www.github.com/solyaris/rChatScript).
BTW, I'm using Ruby 2.4 on a Linux Ubuntu 14.04 server.

I have problems with track_history feature;

see this chunk of code:

require 'tty-prompt'

DEFAULT_COLOR = :bright_blue
PROMPT_STRING =  '>> '
# ...

prompt = TTY::Prompt.new active_color: :green, help_color: :yellow #, track_history: true

puts "CTRL-C to stop"
loop do
  user_say = prompt.ask PROMPT_STRING

  bot_reply = client.volley(user_say, user: user).text

  puts pastel.decorate bot_reply, DEFAULT_COLOR
end

following what said here: https://github.com/piotrmurach/tty-prompt#36-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 previoius or next lines when user presses up/down arrows respectively. :

So, I would expect that using keyboard arrow keys, I could reuse previous line inserted with prompt.ask, instead, it seems to me that arrow keys are not "trapped":

>>  ciao
I don't know what to say.
>>  ^[[A^[[A^[[A^[[A^[[A

My aim is to use TTY:prompt having features similar to those provided by http://bogojoker.com/readline/

Any idea?

Thanks a lot
giorgio

io/console not required by default

Hello,
I use a simple application with TTY::Prompt.ask and TTY::Prompt.select but since a few weeks ago I experience an issue that IO#echo= is called by tty-prompt but not required (imported) before.

This Ruby API page tells that the method is part of io/console and manually requiring this file actually fixes the issue.

Here is the full stack trace:

 /usr/lib/ruby/gems/2.3.0/gems/tty-prompt-0.7.0/lib/tty/prompt/reader/mode.rb:24:in `ensure in echo': undefined method `echo=' for #<IO:<STDIN>> (NoMethodError)
    from /usr/lib/ruby/gems/2.3.0/gems/tty-prompt-0.7.0/lib/tty/prompt/reader/mode.rb:24:in `echo'
    from /usr/lib/ruby/gems/2.3.0/gems/tty-prompt-0.7.0/lib/tty/prompt/reader.rb:119:in `block in read_line'
    from /usr/lib/ruby/gems/2.3.0/gems/tty-prompt-0.7.0/lib/tty/prompt/reader.rb:60:in `buffer'
    from /usr/lib/ruby/gems/2.3.0/gems/tty-prompt-0.7.0/lib/tty/prompt/reader.rb:118:in `read_line'
    from /usr/lib/ruby/gems/2.3.0/gems/tty-prompt-0.7.0/lib/tty/prompt/question.rb:151:in `read_input'
    from /usr/lib/ruby/gems/2.3.0/gems/tty-prompt-0.7.0/lib/tty/prompt/question.rb:134:in `process_input'
    from /usr/lib/ruby/gems/2.3.0/gems/tty-prompt-0.7.0/lib/tty/prompt/question.rb:105:in `render'
    from /usr/lib/ruby/gems/2.3.0/gems/tty-prompt-0.7.0/lib/tty/prompt/question.rb:96:in `call'
    from /usr/lib/ruby/gems/2.3.0/gems/tty-prompt-0.7.0/lib/tty/prompt.rb:103:in `ask'

Multiple issues on Windows

Hi,

not sure if I am missing something obvious here but I get multiple issues when trying TTY::Prompt on Windows 7.

Issues

Specifically, when running the first four examples from the readme - prompt name, like ruby?, secret, destiny - the following happens:

  • Upon requiring tty-prompt, a warning message "Cannot find path." is always printed (in Swedish in the screenshot).
  • The default name is correctly shown but when proceeding, it skips the following yes/no question. The same happens when other questions precede a yes/no question but not when the question is first. (see screenshot)
  • Following questions also overwrite earlier lines (perhaps due to the warning) (see screenshot).
  • The masked input, instead of taking masked characters will beep a number of times equal to the string length (plus line terminators). When used as the first
  • The select prompt does not permit changing options with arrow keys.

Code tested

require 'rubygems' #not needed but included for completeness
#require 'tty' #later test
require 'tty-prompt'

prompt = TTY::Prompt.new
# Following lines reordered and disabled depending on test.
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))

Screenshots

Before entering name
(the first line is a standard Windows warning and translates to "Cannot find path"/"The system cannot find the path specified."?):

image

After entering name and pressing enter once
(note how second prompt is printed but not queried):
image

Environment

  • Win 7 enterprise, SP1
  • ruby 2.3.1p112 2016-04-26 x64-mingw32
  • gem 2.6.7
  • tty-prompt 0.8.0 installed via gem.
  • Later, tty 0.5.0 was added, which pulled in tty-prompt 0.6.0. Issues persisted with a change listed below. Uninstalled all packages and reinstalled only tty-prompt 0.8.0.
  • bundle installed but not used here.
  • Running from cmd.

Additional info

  • Tried installing tty via gem: no change when not required.
  • Requiring tty takes over a minute.
  • When requiring tty, the only observed change is when the masked question is presented first. It will be presented twice - once where it takes input as normal without masking, then a second time while beeping.
  • The "Cannot find path" message does not appear for other gems.

tty-prompt seems like a great tool for making terminal applications otherwise and not sure if others experience this issue. It might just be Windows 7 or the company environment.

Thanks and merry xmas,
Cenny

Add press_any_key prompt

Something like:

any_key('Somethings about to happen, press any key to continue')

and with countdown:

any_key('Somethings about to happen, press any key to continue, continuing automatically in...', countdown: 10)
(which would continue automatically once the countdown reaches zero)

Error when no TTY (Errno::ENOTTY: Inappropriate ioctl for device)

I am hitting an error in my specs as there is no TTY on the build server. I have come up with two ways to address it and would like your input on which you think is best (if either).

Option 1: Update console.rb

def get_char(options)
  return input.getc unless input.tty?
  mode.raw(options[:raw]) do
    mode.echo(options[:echo]) { input.getc }
  end
end

Option 2: Override read_keypress in TestPrompt

def read_keypress(options = {})
  super(options.merge({ echo: true, raw: false }))
end

I am leaning towards Option 1. Let me know and I can submit a PR.

Thanks.

Read line multibyte char

I found some bug. When get multibyte input, it dese not work well.

I think problem is Reader#read_line get char as byte

For example

"한글".bytes # Korean
# => [237, 140, 140, 235, 166, 172]

line = ''
[237, 140, 140, 235, 166, 172] .each { |b| line << b }
#=> "í\u0095\u009Cê¸\u0080"

Solution I think about,

"한글".bytes.pack("C*").force_encoding("utf-8")

# or

line.codepoints.to_a.pack("C*").force_encoding("utf-8")

I suggested #28, but need more graceful way to fix it.
How should I work for this problem?

Change color of certain prompts

I would like to change the color of certain prompts such as the default value prompt and the "use arrow keys to move up and down"

They end up being black which blends in with black terminals.

Ruby 2.4: Title line of select gets repeated when using prompt.publish

I just tried upgrading to Ruby 2.4 and found a weird bug concerning the behaviour of the promt.select menu when working with prompt.publish. Minimal working example:

require "tty-prompt"

prompt = TTY::Prompt.new

prompt.on(:keypress) do |event|
  if event.key.name == 'j'
    prompt.publish(:keydown)
  end

  if event.key.name == 'k'
    prompt.publish(:keyup)
  end
end

choice = prompt.select("Select drinks?", marker: ">") do |menu|
  menu.choice :vodka
  menu.choice :beer
  menu.choice :wine
  menu.choice :whisky
  menu.choice :bourbon
end

puts "Your choice: #{choice}"

Under Ruby 2.3, the behaviour is as expected:

projects/tty-prompt-bug [2h|master] (1) ruby minimal-example.rb
Select drinks? wine
Your choice: wine

When switching to Ruby 2.4, the title line of the select prompt gets printed in a new line each time the selection is changed using j or k:

projects/tty-prompt-bug [2h|master] (1) ruby minimal-example.rb
Select drinks? (Use arrow keys, press Enter to select)
Select drinks?
Select drinks?
Select drinks?
Select drinks?
Select drinks?
Select drinks?
Select drinks?
Select drinks?
Select drinks? wine
Your choice: wine

When I use the arrow-keys instead, the title line gets no extra repititions.

The version of tty-prompt is 0.10.0 in both cases. I compared Ruby 2.3.1p112 with 2.4.0p0.

Add something like per_page: nil, to show all items

I think it would be useful to have a shortcut for disabling pagination and displaying all items in a list.

Potential options:

per_page: nil  # implies no per_page value is set
per_page: :all # descriptive
per_page: 0    # first non-meaningful integer
per_page: -1   # alludes to the final element of an array

Ability to check input inclusion in array

Hi,

Is it possible to check input inclusion in array instead of a range? Something similar to:

available_integers  = [1, 4, 8]

def get_int(available_integers)
  ...
  prompt.keypress('Choose something',
                   in: available_integers) #<= throws 'not a range'
  ...
end

Thanks so much for this gem. Saved my life!

Is there any way can set some choices items disabled?

Thanks for your lib .
And I want to know how to disable some items in list, which acts like the js lib named Inquirer.
Is there any configuration with tty-prompt?

$ node checkbox.js 
? Select toppings 
  = The extras = 
 ◯ Pineapple
 - Olives (out of stock)    <==== this item is disabled
❯◯ Extra cheese
  = The Meats = 
 ◯ Pepperoni
 ◯ Ham
(Move up and down to reveal more choices)

You can clone it and try:

git clone https://github.com/SBoudrias/Inquirer.js.git
yarn 
cd example
node checkbox.js

BTW, Make all prompts(yes, ask, select ...) into one prompt is cool, like Inquirer's way

Expander: Select / Show hint by default

I want to be able to tell the Expander to show the hint for the default option from startup. It seems like this is not possible through the usual API. Would this feature be considered acceptable if a pull request was made?

Move hardware cursor to selection in select/multi_select

The hardware cursor is not moved to the focussed option in select and multi_select, which causes problems in software such as terminal screenreaders for the blind.

require 'tty'
p=TTY::Prompt.new
p.select("Choice?", %w(First Second Third))

newline in zsh after 0.7.0

in versoin 0.7 onwards after using anything in tty-prompt my zsh shell starts adding newlines after return:

0.6.0 version:
0.6.0 version

0.7.0 version:
0.7.0 version

I believe these changes to reader mode have somehow introduced this weird bug: v0.6.0...v0.7.0#diff-d0c27415e6027553e90af174f4d24c98

I do not have enough shell knowledge to figure out what

I tried the same example in bash and sh and the bug is not present there.

Here is a screenshoot of my collegues zsh setup:
0.7.0 version

Multiline input issue

Thanks for upgrading v0.11.0

But, even though I pass empty line in multiline session, it does not return anything and I can not finish multiline session. Could you check? :)

IOError on Windows

Getting this:

`sysread': sysread for buffered IO (IOError)
from C:/Ruby/lib/ruby/gems/2.0.0/gems/tty-prompt-0.7.1/lib/tty/prompt/reader.rb:93
from C:/Ruby/lib/ruby/gems/2.0.0/gems/tty-prompt-0.7.1/lib/tty/prompt/reader.rb:78:in `block (3 levels) in read_keypress'

In reference to kontena/kontena#1401

Browse "select" list by page?

Hello Piotr & community,

I'm using the "select" prompt with a long list (multiple pages) and I'm wondering if it would be somehow possible to page-browse the list similar to the "enum_select" prompt, i.e. pressing page-down key browses to next page of items.

Motivation: I believe the "select' prompt with it's selected-item marker is more intuitive for users to browse and choose an item, especially with a long list.

In any case, thank you for making and sharing this great piece of work!

Cheers,

orachux

Keypress with timeout leaves countdown thread running

..and raises when it reaches zero.

image

Reproduce:

require 'tty-prompt'

TTY::Prompt.new.keypress("Press any key to continue or ctrl-c to cancel (Automatically continuing in :countdown seconds) ...", timeout: 3)

puts "Continuing"
sleep 5
puts "Will never get here if you pressed a key before reaching zero"

Answer Collector broken on Windows

The answer collector is broken on Windows due to the fix for #56. The non-blocking call appears to be causing it to immediately go to the next question in the interview.

windows_answer_collect

You can see our monkey patch in order to make it work here.

huge select list - a way to follow the cursor ?

Hi,
So, I have a huge select list to show, taller than my terminal screen. The issue is that I can't see the top of selection, even if the cursor is on it; does an option exist to fix it ? ( maybe something like htop do)

Undefined method `color?` for TTY::Screen::Class

Hello,

I have a code complaining as soon as I call prompt = TTY::Prompt.new.
Here is the trace:

avril14th@avril14th:~/src/white2$ rake admin:organization:create
rake aborted!
NoMethodError: undefined method `color?' for TTY::Screen:Class
/home/avril14th/.rvm/gems/ruby-2.2.3/gems/pastel-0.5.2/lib/pastel/color.rb:24:in `block in initialize'
/home/avril14th/.rvm/gems/ruby-2.2.3/gems/pastel-0.5.2/lib/pastel/color.rb:24:in `fetch'
/home/avril14th/.rvm/gems/ruby-2.2.3/gems/pastel-0.5.2/lib/pastel/color.rb:24:in `initialize'
/home/avril14th/.rvm/gems/ruby-2.2.3/gems/pastel-0.5.2/lib/pastel.rb:32:in `new'
/home/avril14th/.rvm/gems/ruby-2.2.3/gems/pastel-0.5.2/lib/pastel.rb:32:in `new'
/home/avril14th/.rvm/gems/ruby-2.2.3/gems/tty-prompt-0.3.0/lib/tty/prompt.rb:57:in `initialize'
/home/avril14th/src/white2/lib/tasks/admin.rake:9:in `new'
/home/avril14th/src/white2/lib/tasks/admin.rake:9:in `block (3 levels) in <top (required)>'

I use tty-prompt 0.30 in a Rails 4.2.5 app.

answers

How do you catch user's answers to questions?

multi_select fails when per_page is specified

prompt.multi_select('Choose your favourite drink?', {"test": 1, "test2": 2})
Choose your favourite drink? (Use arrow keys, press Space to select and Enter to finish)
‣ ⬡ test
⬡ test2

prompt.multi_select('Choose your favourite drink?', {"test": 1, "test2": 2}, per_page: 10)
Choose your favourite drink? (Use arrow keys, press Space to select and Enter to finish)
‣ ⬡ [:test, 1]

Default's behavior should be pre-fill in #ask

You can not enter an empty value to a prompt that has a default value.

So instead of treating empty response to mean "use default", maybe the default value should be prefilled to the input prompt.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.