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