File Manager
Viewing File: prompt.rb
# 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