File Manager

Path: /opt/chef/embedded/lib/ruby/gems/2.7.0/gems/tty-prompt-0.21.0/lib/tty/

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