lib/tty/prompt/question.rb in tty-prompt-0.2.0 vs lib/tty/prompt/question.rb in tty-prompt-0.3.0
- old
+ new
@@ -1,289 +1,311 @@
# encoding: utf-8
require 'tty/prompt/question/modifier'
require 'tty/prompt/question/validation'
-require 'tty/prompt/response_delegation'
+require 'tty/prompt/question/checks'
+require 'tty/prompt/converter_dsl'
+require 'tty/prompt/converters'
module TTY
# A class responsible for shell prompt interactions.
class Prompt
- # A class representing a command line question
+ # A class responsible for gathering user input
+ #
+ # @api public
class Question
- include ResponseDelegation
+ include Checks
+ include Converters
+ BLANK_REGEX = /\A[[:space:]]*\z/o.freeze
+
+ UndefinedSetting = Module.new
+
# Store question message
# @api public
attr_reader :message
- # Store default value.
- #
- # @api private
- attr_reader :default_value
-
- attr_reader :validation
-
- # Controls character processing of the answer
- #
- # @api public
attr_reader :modifier
- attr_reader :error
-
- # Returns character mode
- #
- # @api public
- attr_reader :character
-
- # @api private
attr_reader :prompt
+ attr_reader :validation
+
# Initialize a Question
#
# @api public
def initialize(prompt, options = {})
- @prompt = prompt || Prompt.new
- @required = options.fetch(:required) { false }
- @echo = options.fetch(:echo) { true }
- @raw = options.fetch(:raw) { false }
- @mask = options.fetch(:mask) { false }
- @character = options.fetch(:character) { false }
- @in = options.fetch(:in) { false }
- @modifier = Modifier.new options.fetch(:modifier) { [] }
- @validation = Validation.new(options.fetch(:validation) { nil })
- @default = options.fetch(:default) { nil }
- @error = false
- @converter = Necromancer.new
- @read = options.fetch(:read) { nil }
+ @prompt = prompt
+ @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 }
+ @read = options.fetch(:read) { UndefinedSetting }
+ @convert = options.fetch(:convert) { UndefinedSetting }
+ @color = options.fetch(:color) { :green }
+ @done = false
+ @input = nil
+
+ @evaluator = Evaluator.new(self)
+
+ @evaluator << CheckRequired
+ @evaluator << CheckDefault
+ @evaluator << CheckRange
+ @evaluator << CheckValidation
+ @evaluator << CheckModifier
end
- # Call the quesiton
+ # Call the question
#
# @param [String] message
#
# @return [self]
#
# @api public
def call(message, &block)
+ return if blank?(message)
@message = message
block.call(self) if block
- prompt.output.print("#{prompt.prefix}#{message}")
render
end
- # Reader answer and convert to type
+ # Read answer and convert to type
#
# @api private
def render
- dispatch.read_type(@read)
+ until @done
+ render_question
+ result = process_input
+ errors = result.errors
+ render_error_or_finish(result)
+ refresh(errors.count)
+ end
+ render_question
+ convert_result(result.value)
end
- # Set default value.
+ # Render question
#
- # @api public
- def default(value)
- return @default unless value
- @default = value
+ # @api private
+ def render_question
+ header = "#{prompt.prefix}#{message} "
+ if @convert == :bool && !@done
+ header += @prompt.decorate('(Y/n)', :bright_black) + ' '
+ elsif !echo?
+ header
+ elsif @done
+ header += @prompt.decorate("#{@input}", @color)
+ elsif default?
+ header += @prompt.decorate("(#{default})", :bright_black) + ' '
+ end
+ @prompt.print(header)
+ @prompt.print("\n") if @done
end
- # Check if default value is set
+ # Decide how to handle input from user
#
- # @return [Boolean]
+ # @api private
+ def process_input
+ @input = read_input
+ if blank?(@input)
+ @input = default? ? default : nil
+ end
+ @evaluator.(@input)
+ end
+
+ # Process input
#
- # @api public
- def default?
- !!@default
+ # @api private
+ def read_input
+ case @read
+ when :keypress
+ @prompt.read_keypress
+ when :multiline
+ @prompt.read_multiline
+ else
+ @prompt.read_line(echo)
+ end
end
- # Ensure that passed argument is present or not
+ # Handle error condition
#
- # @return [Boolean]
- #
- # @api public
- def required(value)
- @required = value
+ # @api private
+ def render_error_or_finish(result)
+ if result.failure?
+ result.errors.each do |err|
+ @prompt.print(@prompt.clear_line)
+ @prompt.print(@prompt.decorate('>>', :red) + ' ' + err)
+ end
+ @prompt.print(@prompt.cursor.up(result.errors.count))
+ else
+ @done = true
+ if result.errors.count.nonzero?
+ @prompt.print(@prompt.cursor.down(result.errors.count))
+ end
+ end
end
- # Set validation rule for an argument
+ # Determine area of the screen to clear
#
- # @param [Object] value
+ # @param [Integer] errors
#
- # @return [Question]
- #
- # @api public
- def validate(value = nil, &block)
- @validation = Validation.new(value || block)
+ # @api private
+ def refresh(errors = nil)
+ lines = @message.scan("\n").length
+ lines += ((!echo? || errors.nonzero?) ? 1 : 2) # clear user enter
+
+ if errors.nonzero? && @done
+ lines += errors
+ end
+
+ @prompt.print(@prompt.clear_lines(lines))
end
- # Modify string according to the rule given.
+ # Convert value to expected type
#
- # @param [Symbol] rule
+ # @param [Object] value
#
- # @api public
- def modify(*rules)
- @modifier = Modifier.new(*rules)
+ # @api private
+ def convert_result(value)
+ if convert? & !blank?(value)
+ converter_registry.(@convert, value)
+ else
+ value
+ end
end
- # Setup behaviour when error(s) occur
+ # Set reader type
#
# @api public
- def on_error(action = nil)
- @error = action
+ def read(value)
+ @read = value
end
- # Check if error behaviour is set
+ # Specify answer conversion
#
# @api public
- def error?
- !!@error
+ def convert(value)
+ @convert = value
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.
+ # Check if conversion is set
#
+ # @return [Boolean]
+ #
# @api public
- def echo(value = nil)
- return @echo if value.nil?
- @echo = value
+ def convert?
+ @convert != UndefinedSetting
end
- # Chec if echo is set
+ # Set default value.
#
# @api public
- def echo?
- !!@echo
+ def default(value = (not_set = true))
+ return @default if not_set
+ @default = value
end
- # Turn raw mode on or off. This enables character-based input.
+ # Check if default value is set
#
+ # @return [Boolean]
+ #
# @api public
- def raw(value = nil)
- return @raw if value.nil?
- @raw = value
+ def default?
+ @default != UndefinedSetting
end
- # Check if raw mode is set
+ # Ensure that passed argument is present or not
#
+ # @return [Boolean]
+ #
# @api public
- def raw?
- !!@raw
+ def required(value = (not_set = true))
+ return @required if not_set
+ @required = value
end
+ alias_method :required?, :required
- # Set character for masking the STDIN input
+ # Set validation rule for an argument
#
- # @param [String] character
+ # @param [Object] value
#
- # @return [self]
+ # @return [Question]
#
# @api public
- def mask(char = nil)
- return @mask if char.nil?
- @mask = char
+ def validate(value = nil, &block)
+ @validation = (value || block)
end
- # Check if character mask is set
+ def validation?
+ @validation != UndefinedSetting
+ end
+
+ # Modify string according to the rule given.
#
- # @return [Boolean]
+ # @param [Symbol] rule
#
# @api public
- def mask?
- !!@mask
+ def modify(*rules)
+ @modifier = rules
end
- # Set if the input is character based or not
+ # 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.
#
- # @param [Boolean] value
- #
- # @return [self]
- #
# @api public
- def char(value = nil)
- return @character if value.nil?
- @character = value
+ def echo(value = nil)
+ return @echo if value.nil?
+ @echo = value
end
+ alias_method :echo?, :echo
- # Check if character intput is set
+ # Turn raw mode on or off. This enables character-based input.
#
- # @return [Boolean]
- #
# @api public
- def character?
- !!@character
+ def raw(value = nil)
+ return @raw if value.nil?
+ @raw = value
end
+ alias_method :raw?, :raw
- # Set expect range of values
+ # Set expected range of values
#
# @param [String] value
#
# @api public
- def in(value = nil)
- return @in if value.nil?
- @in = @converter.convert(value).to(:range, strict: true)
+ def in(value = (not_set = true))
+ if in? && !@in.is_a?(Range)
+ @in = converter_registry.(:range, @in)
+ end
+ return @in if not_set
+ @in = converter_registry.(:range, value)
end
# Check if range is set
#
# @return [Boolean]
#
# @api public
def in?
- !!@in
+ @in != UndefinedSetting
end
- # Check if response matches all the requirements set by the question
- #
- # @param [Object] value
- #
- # @return [Object]
- #
- # @api private
- def evaluate_response(input)
- return @default if !input && default?
- check_required(input)
- return if input.nil?
-
- within?(input)
- validation.(input)
- modifier.apply_to(input)
+ def blank?(value)
+ value.nil? ||
+ value.respond_to?(:empty?) && value.empty? ||
+ BLANK_REGEX === value
end
- # Reset question object.
- #
- # @api public
- def clean
- @message = nil
- @default = nil
- @required = false
- @modifier = nil
- end
-
def to_s
"#{message}"
end
+ # String representation of this question
+ # @api public
def inspect
- "#<Question @message=#{message}>"
- end
-
- private
-
- # Check if value is present
- #
- # @api private
- def check_required(value)
- if @required && !default? && value.nil?
- fail ArgumentRequired, 'No value provided for required'
- end
- end
-
- # Check if value is within expected range
- #
- # @api private
- def within?(value)
- if in? && value
- @in.include?(value) || fail(InvalidArgument,
- "Value #{value} is not included in the range #{@in}")
- end
+ "#<#{self.class.name} @message=#{message}, @input=#{@input}>"
end
end # Question
end # Prompt
end # TTY