tty-prompt 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +66 -7
- data/examples/key_events.rb +11 -0
- data/examples/keypress.rb +3 -5
- data/examples/multiline.rb +9 -0
- data/examples/pause.rb +7 -0
- data/lib/tty/prompt.rb +82 -44
- data/lib/tty/prompt/confirm_question.rb +20 -36
- data/lib/tty/prompt/enum_list.rb +32 -23
- data/lib/tty/prompt/expander.rb +35 -31
- data/lib/tty/prompt/keypress.rb +91 -0
- data/lib/tty/prompt/list.rb +38 -23
- data/lib/tty/prompt/mask_question.rb +4 -7
- data/lib/tty/prompt/multi_list.rb +3 -1
- data/lib/tty/prompt/multiline.rb +71 -0
- data/lib/tty/prompt/question.rb +33 -35
- data/lib/tty/prompt/reader.rb +154 -38
- data/lib/tty/prompt/reader/codes.rb +4 -4
- data/lib/tty/prompt/reader/console.rb +1 -1
- data/lib/tty/prompt/reader/history.rb +145 -0
- data/lib/tty/prompt/reader/key_event.rb +4 -0
- data/lib/tty/prompt/reader/line.rb +162 -0
- data/lib/tty/prompt/reader/mode.rb +2 -2
- data/lib/tty/prompt/reader/win_console.rb +5 -1
- data/lib/tty/prompt/slider.rb +18 -12
- data/lib/tty/prompt/timeout.rb +48 -0
- data/lib/tty/prompt/version.rb +1 -1
- data/spec/unit/ask_spec.rb +15 -0
- data/spec/unit/converters/convert_bool_spec.rb +1 -0
- data/spec/unit/keypress_spec.rb +35 -6
- data/spec/unit/multi_select_spec.rb +18 -0
- data/spec/unit/multiline_spec.rb +67 -9
- data/spec/unit/question/default_spec.rb +1 -0
- data/spec/unit/question/echo_spec.rb +8 -0
- data/spec/unit/question/in_spec.rb +13 -0
- data/spec/unit/question/required_spec.rb +31 -2
- data/spec/unit/question/validate_spec.rb +39 -9
- data/spec/unit/reader/history_spec.rb +172 -0
- data/spec/unit/reader/key_event_spec.rb +12 -8
- data/spec/unit/reader/line_spec.rb +110 -0
- data/spec/unit/reader/publish_keypress_event_spec.rb +11 -0
- data/spec/unit/reader/read_line_spec.rb +32 -2
- data/spec/unit/reader/read_multiline_spec.rb +21 -7
- data/spec/unit/select_spec.rb +40 -1
- data/spec/unit/yes_no_spec.rb +48 -4
- metadata +14 -3
- data/lib/tty/prompt/history.rb +0 -16
| @@ -76,12 +76,14 @@ module TTY | |
| 76 76 | 
             
                  # @return [Array[nil,Object]]
         | 
| 77 77 | 
             
                  #
         | 
| 78 78 | 
             
                  # @api private
         | 
| 79 | 
            -
                  def  | 
| 79 | 
            +
                  def answer
         | 
| 80 80 | 
             
                    @selected.map(&:value)
         | 
| 81 81 | 
             
                  end
         | 
| 82 82 |  | 
| 83 83 | 
             
                  # Render menu with choices to select from
         | 
| 84 84 | 
             
                  #
         | 
| 85 | 
            +
                  # @return [String]
         | 
| 86 | 
            +
                  #
         | 
| 85 87 | 
             
                  # @api private
         | 
| 86 88 | 
             
                  def render_menu
         | 
| 87 89 | 
             
                    output = ''
         | 
| @@ -0,0 +1,71 @@ | |
| 1 | 
            +
            # encoding: utf-8
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'question'
         | 
| 4 | 
            +
            require_relative 'symbols'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module TTY
         | 
| 7 | 
            +
              class Prompt
         | 
| 8 | 
            +
                # A prompt responsible for multi line user input
         | 
| 9 | 
            +
                #
         | 
| 10 | 
            +
                # @api private
         | 
| 11 | 
            +
                class Multiline < Question
         | 
| 12 | 
            +
                  HELP = '(Press CTRL-D or CTRL-Z to finish)'.freeze
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def initialize(prompt, options = {})
         | 
| 15 | 
            +
                    super
         | 
| 16 | 
            +
                    @help         = options[:help] || self.class::HELP
         | 
| 17 | 
            +
                    @first_render = true
         | 
| 18 | 
            +
                    @lines_count  = 0
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    @prompt.subscribe(self)
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  # Provide help information
         | 
| 24 | 
            +
                  #
         | 
| 25 | 
            +
                  # @return [String]
         | 
| 26 | 
            +
                  #
         | 
| 27 | 
            +
                  # @api public
         | 
| 28 | 
            +
                  def help(value = (not_set = true))
         | 
| 29 | 
            +
                    return @help if not_set
         | 
| 30 | 
            +
                    @help = value
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  def read_input
         | 
| 34 | 
            +
                    @prompt.read_multiline
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def keyreturn(*)
         | 
| 38 | 
            +
                    @lines_count += 1
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
                  alias keyenter keyreturn
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  def render_question
         | 
| 43 | 
            +
                    header = "#{@prefix}#{message} "
         | 
| 44 | 
            +
                    if !echo?
         | 
| 45 | 
            +
                      header
         | 
| 46 | 
            +
                    elsif @done
         | 
| 47 | 
            +
                      header += @prompt.decorate("#{@input}", @active_color)
         | 
| 48 | 
            +
                    elsif @first_render
         | 
| 49 | 
            +
                      header += @prompt.decorate(help, @help_color)
         | 
| 50 | 
            +
                      @first_render = false
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
                    header += "\n"
         | 
| 53 | 
            +
                    header
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def process_input(question)
         | 
| 57 | 
            +
                    @lines = read_input
         | 
| 58 | 
            +
                    @input = "#{@lines.first.strip} ..." unless @lines.first.to_s.empty?
         | 
| 59 | 
            +
                    if Utils.blank?(@input)
         | 
| 60 | 
            +
                      @input = default? ? default : nil
         | 
| 61 | 
            +
                    end
         | 
| 62 | 
            +
                    @evaluator.(@lines)
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def refresh(lines)
         | 
| 66 | 
            +
                    size = @lines_count + lines + 1
         | 
| 67 | 
            +
                    @prompt.clear_lines(size)
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
                end # Multiline
         | 
| 70 | 
            +
              end # Prompt
         | 
| 71 | 
            +
            end # TTY
         | 
    
        data/lib/tty/prompt/question.rb
    CHANGED
    
    | @@ -16,7 +16,12 @@ module TTY | |
| 16 16 | 
             
                class Question
         | 
| 17 17 | 
             
                  include Checks
         | 
| 18 18 |  | 
| 19 | 
            -
                  UndefinedSetting =  | 
| 19 | 
            +
                  UndefinedSetting = Class.new do
         | 
| 20 | 
            +
                    def to_s
         | 
| 21 | 
            +
                      "undefined"
         | 
| 22 | 
            +
                    end
         | 
| 23 | 
            +
                    alias inspect to_s
         | 
| 24 | 
            +
                  end
         | 
| 20 25 |  | 
| 21 26 | 
             
                  # Store question message
         | 
| 22 27 | 
             
                  # @api public
         | 
| @@ -38,7 +43,6 @@ module TTY | |
| 38 43 | 
             
                    @in         = options.fetch(:in) { UndefinedSetting }
         | 
| 39 44 | 
             
                    @modifier   = options.fetch(:modifier) { [] }
         | 
| 40 45 | 
             
                    @validation = options.fetch(:validation) { UndefinedSetting }
         | 
| 41 | 
            -
                    @read       = options.fetch(:read) { UndefinedSetting }
         | 
| 42 46 | 
             
                    @convert    = options.fetch(:convert) { UndefinedSetting }
         | 
| 43 47 | 
             
                    @active_color = options.fetch(:active_color) { @prompt.active_color }
         | 
| 44 48 | 
             
                    @help_color = options.fetch(:help_color) { @prompt.help_color }
         | 
| @@ -103,22 +107,25 @@ module TTY | |
| 103 107 | 
             
                  def render
         | 
| 104 108 | 
             
                    @errors = []
         | 
| 105 109 | 
             
                    until @done
         | 
| 106 | 
            -
                       | 
| 107 | 
            -
                       | 
| 110 | 
            +
                      question = render_question
         | 
| 111 | 
            +
                      @prompt.print(question)
         | 
| 112 | 
            +
                      result = process_input(question)
         | 
| 108 113 | 
             
                      if result.failure?
         | 
| 109 114 | 
             
                        @errors = result.errors
         | 
| 110 | 
            -
                        render_error(result.errors)
         | 
| 115 | 
            +
                        @prompt.print(render_error(result.errors))
         | 
| 111 116 | 
             
                      else
         | 
| 112 117 | 
             
                        @done = true
         | 
| 113 118 | 
             
                      end
         | 
| 114 | 
            -
                      refresh(lines)
         | 
| 119 | 
            +
                      @prompt.print(refresh(question.lines.count))
         | 
| 115 120 | 
             
                    end
         | 
| 116 | 
            -
                    render_question
         | 
| 121 | 
            +
                    @prompt.print(render_question)
         | 
| 117 122 | 
             
                    convert_result(result.value)
         | 
| 118 123 | 
             
                  end
         | 
| 119 124 |  | 
| 120 125 | 
             
                  # Render question
         | 
| 121 126 | 
             
                  #
         | 
| 127 | 
            +
                  # @return [String]
         | 
| 128 | 
            +
                  #
         | 
| 122 129 | 
             
                  # @api private
         | 
| 123 130 | 
             
                  def render_question
         | 
| 124 131 | 
             
                    header = "#{@prefix}#{message} "
         | 
| @@ -129,17 +136,15 @@ module TTY | |
| 129 136 | 
             
                    elsif default? && !Utils.blank?(@default)
         | 
| 130 137 | 
             
                      header += @prompt.decorate("(#{default})", @help_color) + ' '
         | 
| 131 138 | 
             
                    end
         | 
| 132 | 
            -
                    @ | 
| 133 | 
            -
                     | 
| 134 | 
            -
             | 
| 135 | 
            -
                    header.lines.count + (@done ? 1 : 0)
         | 
| 139 | 
            +
                    header << "\n" if @done
         | 
| 140 | 
            +
                    header
         | 
| 136 141 | 
             
                  end
         | 
| 137 142 |  | 
| 138 143 | 
             
                  # Decide how to handle input from user
         | 
| 139 144 | 
             
                  #
         | 
| 140 145 | 
             
                  # @api private
         | 
| 141 | 
            -
                  def process_input
         | 
| 142 | 
            -
                    @input = read_input
         | 
| 146 | 
            +
                  def process_input(question)
         | 
| 147 | 
            +
                    @input = read_input(question)
         | 
| 143 148 | 
             
                    if Utils.blank?(@input)
         | 
| 144 149 | 
             
                      @input = default? ? default : nil
         | 
| 145 150 | 
             
                    end
         | 
| @@ -149,43 +154,43 @@ module TTY | |
| 149 154 | 
             
                  # Process input
         | 
| 150 155 | 
             
                  #
         | 
| 151 156 | 
             
                  # @api private
         | 
| 152 | 
            -
                  def read_input
         | 
| 153 | 
            -
                     | 
| 154 | 
            -
                    when :keypress
         | 
| 155 | 
            -
                      @prompt.read_keypress
         | 
| 156 | 
            -
                    when :multiline
         | 
| 157 | 
            -
                      @prompt.read_multiline.each(&:chomp!)
         | 
| 158 | 
            -
                    else
         | 
| 159 | 
            -
                      @prompt.read_line(echo: echo).chomp
         | 
| 160 | 
            -
                    end
         | 
| 157 | 
            +
                  def read_input(question)
         | 
| 158 | 
            +
                    @prompt.read_line(question, echo: echo).chomp
         | 
| 161 159 | 
             
                  end
         | 
| 162 160 |  | 
| 163 161 | 
             
                  # Handle error condition
         | 
| 164 162 | 
             
                  #
         | 
| 163 | 
            +
                  # @return [String]
         | 
| 164 | 
            +
                  #
         | 
| 165 165 | 
             
                  # @api private
         | 
| 166 166 | 
             
                  def render_error(errors)
         | 
| 167 | 
            -
                    errors. | 
| 167 | 
            +
                    errors.reduce('') do |acc, err|
         | 
| 168 168 | 
             
                      newline = (@echo ? '' : "\n")
         | 
| 169 | 
            -
                       | 
| 169 | 
            +
                      acc << newline + @prompt.decorate('>>', :red) + ' ' + err
         | 
| 170 | 
            +
                      acc
         | 
| 170 171 | 
             
                    end
         | 
| 171 172 | 
             
                  end
         | 
| 172 173 |  | 
| 173 174 | 
             
                  # Determine area of the screen to clear
         | 
| 174 175 | 
             
                  #
         | 
| 175 | 
            -
                  # @param [ | 
| 176 | 
            +
                  # @param [Integer] lines
         | 
| 177 | 
            +
                  #   number of lines to clear
         | 
| 178 | 
            +
                  #
         | 
| 179 | 
            +
                  # @return [String]
         | 
| 176 180 | 
             
                  #
         | 
| 177 181 | 
             
                  # @api private
         | 
| 178 182 | 
             
                  def refresh(lines)
         | 
| 183 | 
            +
                    output = ''
         | 
| 179 184 | 
             
                    if @done
         | 
| 180 185 | 
             
                      if @errors.count.zero? && @echo
         | 
| 181 | 
            -
                        @prompt. | 
| 186 | 
            +
                        output << @prompt.cursor.up(lines)
         | 
| 182 187 | 
             
                      else
         | 
| 183 188 | 
             
                        lines += @errors.count
         | 
| 184 189 | 
             
                      end
         | 
| 185 190 | 
             
                    else
         | 
| 186 | 
            -
                      @prompt. | 
| 191 | 
            +
                      output << @prompt.cursor.up(lines)
         | 
| 187 192 | 
             
                    end
         | 
| 188 | 
            -
                    @prompt. | 
| 193 | 
            +
                    output + @prompt.clear_lines(lines)
         | 
| 189 194 | 
             
                  end
         | 
| 190 195 |  | 
| 191 196 | 
             
                  # Convert value to expected type
         | 
| @@ -201,13 +206,6 @@ module TTY | |
| 201 206 | 
             
                    end
         | 
| 202 207 | 
             
                  end
         | 
| 203 208 |  | 
| 204 | 
            -
                  # Set reader type
         | 
| 205 | 
            -
                  #
         | 
| 206 | 
            -
                  # @api public
         | 
| 207 | 
            -
                  def read(value)
         | 
| 208 | 
            -
                    @read = value
         | 
| 209 | 
            -
                  end
         | 
| 210 | 
            -
             | 
| 211 209 | 
             
                  # Specify answer conversion
         | 
| 212 210 | 
             
                  #
         | 
| 213 211 | 
             
                  # @api public
         | 
    
        data/lib/tty/prompt/reader.rb
    CHANGED
    
    | @@ -3,6 +3,8 @@ | |
| 3 3 | 
             
            require 'wisper'
         | 
| 4 4 | 
             
            require 'rbconfig'
         | 
| 5 5 |  | 
| 6 | 
            +
            require_relative 'reader/history'
         | 
| 7 | 
            +
            require_relative 'reader/line'
         | 
| 6 8 | 
             
            require_relative 'reader/key_event'
         | 
| 7 9 | 
             
            require_relative 'reader/console'
         | 
| 8 10 | 
             
            require_relative 'reader/win_console'
         | 
| @@ -29,6 +31,11 @@ module TTY | |
| 29 31 |  | 
| 30 32 | 
             
                  attr_reader :env
         | 
| 31 33 |  | 
| 34 | 
            +
                  attr_reader :track_history
         | 
| 35 | 
            +
                  alias track_history? track_history
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  attr_reader :console
         | 
| 38 | 
            +
             | 
| 32 39 | 
             
                  # Key codes
         | 
| 33 40 | 
             
                  CARRIAGE_RETURN = 13
         | 
| 34 41 | 
             
                  NEWLINE         = 10
         | 
| @@ -37,13 +44,42 @@ module TTY | |
| 37 44 |  | 
| 38 45 | 
             
                  # Initialize a Reader
         | 
| 39 46 | 
             
                  #
         | 
| 47 | 
            +
                  # @param [IO] input
         | 
| 48 | 
            +
                  #   the input stream
         | 
| 49 | 
            +
                  # @param [IO] output
         | 
| 50 | 
            +
                  #   the output stream
         | 
| 51 | 
            +
                  # @param [Hash] options
         | 
| 52 | 
            +
                  # @option options [Symbol] :interrupt
         | 
| 53 | 
            +
                  #   handling of Ctrl+C key out of :signal, :exit, :noop
         | 
| 54 | 
            +
                  # @option options [Boolean] :track_history
         | 
| 55 | 
            +
                  #   disable line history tracking, true by default
         | 
| 56 | 
            +
                  #
         | 
| 40 57 | 
             
                  # @api public
         | 
| 41 58 | 
             
                  def initialize(input = $stdin, output = $stdout, options = {})
         | 
| 42 59 | 
             
                    @input     = input
         | 
| 43 60 | 
             
                    @output    = output
         | 
| 44 61 | 
             
                    @interrupt = options.fetch(:interrupt) { :error }
         | 
| 45 62 | 
             
                    @env       = options.fetch(:env) { ENV }
         | 
| 46 | 
            -
                    @ | 
| 63 | 
            +
                    @track_history = options.fetch(:track_history) { true }
         | 
| 64 | 
            +
                    @console   = select_console(input)
         | 
| 65 | 
            +
                    @history   = History.new do |h|
         | 
| 66 | 
            +
                      h.duplicates = false
         | 
| 67 | 
            +
                      h.exclude = proc { |line| line.strip == '' }
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
                    @stop = false # gathering input
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    subscribe(self)
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  # Select appropriate console
         | 
| 75 | 
            +
                  #
         | 
| 76 | 
            +
                  # @api private
         | 
| 77 | 
            +
                  def select_console(input)
         | 
| 78 | 
            +
                    if windows? && !env['TTY_TEST']
         | 
| 79 | 
            +
                      WinConsole.new(input)
         | 
| 80 | 
            +
                    else
         | 
| 81 | 
            +
                      Console.new(input)
         | 
| 82 | 
            +
                    end
         | 
| 47 83 | 
             
                  end
         | 
| 48 84 |  | 
| 49 85 | 
             
                  # Get input in unbuffered mode.
         | 
| @@ -83,7 +119,7 @@ module TTY | |
| 83 119 | 
             
                    char  = codes ? codes.pack('U*') : nil
         | 
| 84 120 |  | 
| 85 121 | 
             
                    trigger_key_event(char) if char
         | 
| 86 | 
            -
                    handle_interrupt if char ==  | 
| 122 | 
            +
                    handle_interrupt if char == console.keys[:ctrl_c]
         | 
| 87 123 | 
             
                    char
         | 
| 88 124 | 
             
                  end
         | 
| 89 125 | 
             
                  alias read_char read_keypress
         | 
| @@ -98,7 +134,7 @@ module TTY | |
| 98 134 | 
             
                  # @api private
         | 
| 99 135 | 
             
                  def get_codes(options = {}, codes = [])
         | 
| 100 136 | 
             
                    opts = { echo: true, raw: false }.merge(options)
         | 
| 101 | 
            -
                    char =  | 
| 137 | 
            +
                    char = console.get_char(opts)
         | 
| 102 138 | 
             
                    return if char.nil?
         | 
| 103 139 | 
             
                    codes << char.ord
         | 
| 104 140 |  | 
| @@ -108,7 +144,7 @@ module TTY | |
| 108 144 | 
             
                      !(64..126).include?(codes.last)
         | 
| 109 145 | 
             
                    }
         | 
| 110 146 |  | 
| 111 | 
            -
                    while  | 
| 147 | 
            +
                    while console.escape_codes.any?(&condition)
         | 
| 112 148 | 
             
                      get_codes(options, codes)
         | 
| 113 149 | 
             
                    end
         | 
| 114 150 | 
             
                    codes
         | 
| @@ -118,60 +154,110 @@ module TTY | |
| 118 154 | 
             
                  # back to the shell. The input terminates when enter or
         | 
| 119 155 | 
             
                  # return key is pressed.
         | 
| 120 156 | 
             
                  #
         | 
| 157 | 
            +
                  # @param [String] prompt
         | 
| 158 | 
            +
                  #   the prompt to display before input
         | 
| 159 | 
            +
                  #
         | 
| 121 160 | 
             
                  # @param [Boolean] echo
         | 
| 122 161 | 
             
                  #   if true echo back characters, output nothing otherwise
         | 
| 123 162 | 
             
                  #
         | 
| 124 163 | 
             
                  # @return [String]
         | 
| 125 164 | 
             
                  #
         | 
| 126 165 | 
             
                  # @api public
         | 
| 127 | 
            -
                  def read_line( | 
| 128 | 
            -
                     | 
| 129 | 
            -
                     | 
| 130 | 
            -
                     | 
| 131 | 
            -
                     | 
| 166 | 
            +
                  def read_line(*args)
         | 
| 167 | 
            +
                    options = args.last.respond_to?(:to_hash) ? args.pop : {}
         | 
| 168 | 
            +
                    prompt = args.empty? ? '' : args.pop
         | 
| 169 | 
            +
                    opts = { echo: true, raw: true }.merge(options)
         | 
| 170 | 
            +
                    line = Line.new('')
         | 
| 171 | 
            +
                    ctrls = console.keys.keys.grep(/ctrl/)
         | 
| 172 | 
            +
                    clear_line = "\e[2K\e[1G"
         | 
| 132 173 |  | 
| 133 | 
            -
                    while (codes = get_codes(opts)) && (code = codes[0])
         | 
| 174 | 
            +
                    while (codes = unbufferred { get_codes(opts) }) && (code = codes[0])
         | 
| 134 175 | 
             
                      char = codes.pack('U*')
         | 
| 135 176 | 
             
                      trigger_key_event(char)
         | 
| 136 177 |  | 
| 137 | 
            -
                      if  | 
| 138 | 
            -
                        line. | 
| 139 | 
            -
                         | 
| 178 | 
            +
                      if console.keys[:backspace] == char || BACKSPACE == code
         | 
| 179 | 
            +
                        next if line.start?
         | 
| 180 | 
            +
                        line.left
         | 
| 181 | 
            +
                        line.delete
         | 
| 182 | 
            +
                      elsif console.keys[:delete] == char || DELETE == code
         | 
| 183 | 
            +
                        line.delete
         | 
| 184 | 
            +
                      elsif [console.keys[:ctrl_d],
         | 
| 185 | 
            +
                             console.keys[:ctrl_z]].include?(char)
         | 
| 186 | 
            +
                        break
         | 
| 187 | 
            +
                      elsif console.keys[:ctrl_c] == char
         | 
| 188 | 
            +
                        handle_interrupt
         | 
| 189 | 
            +
                      elsif ctrls.include?(console.keys.key(char))
         | 
| 190 | 
            +
                        # skip
         | 
| 191 | 
            +
                      elsif console.keys[:up] == char
         | 
| 192 | 
            +
                        next unless history_previous?
         | 
| 193 | 
            +
                        line.replace(history_previous)
         | 
| 194 | 
            +
                      elsif console.keys[:down] == char
         | 
| 195 | 
            +
                        line.replace(history_next? ? history_next : '')
         | 
| 196 | 
            +
                      elsif console.keys[:left] == char
         | 
| 197 | 
            +
                        line.left
         | 
| 198 | 
            +
                      elsif console.keys[:right] == char
         | 
| 199 | 
            +
                        line.right
         | 
| 140 200 | 
             
                      else
         | 
| 141 | 
            -
                         | 
| 142 | 
            -
             | 
| 201 | 
            +
                        if opts[:raw] && code == CARRIAGE_RETURN
         | 
| 202 | 
            +
                          char = "\n"
         | 
| 203 | 
            +
                          line.move_to_end
         | 
| 204 | 
            +
                        end
         | 
| 205 | 
            +
                        line.insert(char)
         | 
| 206 | 
            +
                      end
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                      if opts[:raw] && opts[:echo]
         | 
| 209 | 
            +
                        output.print(clear_line)
         | 
| 210 | 
            +
                        output.print(prompt + line.to_s)
         | 
| 211 | 
            +
                        if char == "\n"
         | 
| 212 | 
            +
                          line.move_to_start
         | 
| 213 | 
            +
                        elsif !line.end?
         | 
| 214 | 
            +
                          output.print("\e[#{line.size - line.cursor}D")
         | 
| 215 | 
            +
                        end
         | 
| 143 216 | 
             
                      end
         | 
| 144 217 |  | 
| 145 218 | 
             
                      break if (code == CARRIAGE_RETURN || code == NEWLINE)
         | 
| 146 219 |  | 
| 147 | 
            -
                      if  | 
| 148 | 
            -
                         | 
| 220 | 
            +
                      if (console.keys[:backspace] == char || BACKSPACE == code) && opts[:echo]
         | 
| 221 | 
            +
                        if opts[:raw]
         | 
| 222 | 
            +
                          output.print("\e[1X") unless line.start?
         | 
| 223 | 
            +
                        else
         | 
| 224 | 
            +
                          output.print(?\s + (line.start? ? '' :  ?\b))
         | 
| 225 | 
            +
                        end
         | 
| 149 226 | 
             
                      end
         | 
| 150 227 | 
             
                    end
         | 
| 151 | 
            -
                    line
         | 
| 228 | 
            +
                    add_to_history(line.to_s.rstrip) if track_history?
         | 
| 229 | 
            +
                    line.to_s
         | 
| 152 230 | 
             
                  end
         | 
| 153 231 |  | 
| 154 | 
            -
                  # Read multiple lines and  | 
| 232 | 
            +
                  # Read multiple lines and return them in an array.
         | 
| 233 | 
            +
                  # Skip empty lines in the returned lines array.
         | 
| 234 | 
            +
                  # The input gathering is terminated by Ctrl+d or Ctrl+z.
         | 
| 235 | 
            +
                  #
         | 
| 236 | 
            +
                  # @param [String] prompt
         | 
| 237 | 
            +
                  #   the prompt displayed before the input
         | 
| 155 238 | 
             
                  #
         | 
| 156 239 | 
             
                  # @yield [String] line
         | 
| 157 240 | 
             
                  #
         | 
| 158 241 | 
             
                  # @return [Array[String]]
         | 
| 159 242 | 
             
                  #
         | 
| 160 243 | 
             
                  # @api public
         | 
| 161 | 
            -
                  def read_multiline
         | 
| 162 | 
            -
                     | 
| 244 | 
            +
                  def read_multiline(prompt = '')
         | 
| 245 | 
            +
                    @stop = false
         | 
| 246 | 
            +
                    lines = []
         | 
| 163 247 | 
             
                    loop do
         | 
| 164 | 
            -
                      line = read_line
         | 
| 248 | 
            +
                      line = read_line(prompt)
         | 
| 165 249 | 
             
                      break if !line || line == ''
         | 
| 166 | 
            -
                      next  if line !~ /\S/
         | 
| 250 | 
            +
                      next  if line !~ /\S/ && !@stop
         | 
| 167 251 | 
             
                      if block_given?
         | 
| 168 | 
            -
                        yield(line)
         | 
| 252 | 
            +
                        yield(line) unless line.to_s.empty?
         | 
| 169 253 | 
             
                      else
         | 
| 170 | 
            -
                         | 
| 254 | 
            +
                        lines << line unless line.to_s.empty?
         | 
| 171 255 | 
             
                      end
         | 
| 256 | 
            +
                      break if @stop
         | 
| 172 257 | 
             
                    end
         | 
| 173 | 
            -
                     | 
| 258 | 
            +
                    lines
         | 
| 174 259 | 
             
                  end
         | 
| 260 | 
            +
                  alias read_lines read_multiline
         | 
| 175 261 |  | 
| 176 262 | 
             
                  # Expose event broadcasting
         | 
| 177 263 | 
             
                  #
         | 
| @@ -180,18 +266,35 @@ module TTY | |
| 180 266 | 
             
                    publish(event, *args)
         | 
| 181 267 | 
             
                  end
         | 
| 182 268 |  | 
| 183 | 
            -
                  #  | 
| 269 | 
            +
                  # Capture Ctrl+d and Ctrl+z key events
         | 
| 184 270 | 
             
                  #
         | 
| 185 | 
            -
                  # @ | 
| 186 | 
            -
                   | 
| 187 | 
            -
             | 
| 188 | 
            -
                   | 
| 189 | 
            -
                   | 
| 190 | 
            -
             | 
| 191 | 
            -
                  def  | 
| 192 | 
            -
                     | 
| 193 | 
            -
             | 
| 194 | 
            -
             | 
| 271 | 
            +
                  # @api private
         | 
| 272 | 
            +
                  def keyctrl_d(*)
         | 
| 273 | 
            +
                    @stop = true
         | 
| 274 | 
            +
                  end
         | 
| 275 | 
            +
                  alias keyctrl_z keyctrl_d
         | 
| 276 | 
            +
             | 
| 277 | 
            +
                  def add_to_history(line)
         | 
| 278 | 
            +
                    @history.push(line)
         | 
| 279 | 
            +
                  end
         | 
| 280 | 
            +
             | 
| 281 | 
            +
                  def history_next?
         | 
| 282 | 
            +
                    @history.next?
         | 
| 283 | 
            +
                  end
         | 
| 284 | 
            +
             | 
| 285 | 
            +
                  def history_next
         | 
| 286 | 
            +
                    @history.next
         | 
| 287 | 
            +
                    @history.get
         | 
| 288 | 
            +
                  end
         | 
| 289 | 
            +
             | 
| 290 | 
            +
                  def history_previous?
         | 
| 291 | 
            +
                    @history.previous?
         | 
| 292 | 
            +
                  end
         | 
| 293 | 
            +
             | 
| 294 | 
            +
                  def history_previous
         | 
| 295 | 
            +
                    line = @history.get
         | 
| 296 | 
            +
                    @history.previous
         | 
| 297 | 
            +
                    line
         | 
| 195 298 | 
             
                  end
         | 
| 196 299 |  | 
| 197 300 | 
             
                  # Inspect class name and public attributes
         | 
| @@ -204,6 +307,20 @@ module TTY | |
| 204 307 |  | 
| 205 308 | 
             
                  private
         | 
| 206 309 |  | 
| 310 | 
            +
                  # Publish event
         | 
| 311 | 
            +
                  #
         | 
| 312 | 
            +
                  # @param [String] char
         | 
| 313 | 
            +
                  #   the key pressed
         | 
| 314 | 
            +
                  #
         | 
| 315 | 
            +
                  # @return [nil]
         | 
| 316 | 
            +
                  #
         | 
| 317 | 
            +
                  # @api private
         | 
| 318 | 
            +
                  def trigger_key_event(char)
         | 
| 319 | 
            +
                    event = KeyEvent.from(console.keys, char)
         | 
| 320 | 
            +
                    trigger(:"key#{event.key.name}", event) if event.trigger?
         | 
| 321 | 
            +
                    trigger(:keypress, event)
         | 
| 322 | 
            +
                  end
         | 
| 323 | 
            +
             | 
| 207 324 | 
             
                  # Handle input interrupt based on provided value
         | 
| 208 325 | 
             
                  #
         | 
| 209 326 | 
             
                  # @api private
         | 
| @@ -228,7 +345,6 @@ module TTY | |
| 228 345 | 
             
                  #
         | 
| 229 346 | 
             
                  # @api public
         | 
| 230 347 | 
             
                  def windows?
         | 
| 231 | 
            -
                    return false if env["TTY_TEST"] == true
         | 
| 232 348 | 
             
                    ::File::ALT_SEPARATOR == "\\"
         | 
| 233 349 | 
             
                  end
         | 
| 234 350 | 
             
                end # Reader
         |