class TTY::Prompt::Reader

A class responsible for reading character input from STDIN

Used internally to provide key and line reading functionality

@api private

Constants

BACKSPACE
CARRIAGE_RETURN

Key codes

DELETE
InputInterrupt

Raised when the user hits the interrupt key(Control-C)

@api public

NEWLINE

Attributes

console[R]
env[R]
input[R]
output[R]
track_history[R]
track_history?[R]

Public Class Methods

new(input = $stdin, output = $stdout, options = {}) click to toggle source

Initialize a Reader

@param [IO] input

the input stream

@param [IO] output

the output stream

@param [Hash] options @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

@api public

# File lib/tty/prompt/reader.rb, line 58
def initialize(input = $stdin, output = $stdout, options = {})
  @input     = input
  @output    = output
  @interrupt = options.fetch(:interrupt) { :error }
  @env       = options.fetch(:env) { ENV }
  @track_history = options.fetch(:track_history) { true }
  @console   = select_console(input)
  @history   = History.new do |h|
    h.duplicates = false
    h.exclude = proc { |line| line.strip == '' }
  end
  @stop = false # gathering input

  subscribe(self)
end

Public Instance Methods

add_to_history(line) click to toggle source
# File lib/tty/prompt/reader.rb, line 277
def add_to_history(line)
  @history.push(line)
end
get_codes(options = {}, codes = []) click to toggle source

Get input code points

@param [Hash] options @param [Array] codes

@return [Array]

@api private

# File lib/tty/prompt/reader.rb, line 135
def get_codes(options = {}, codes = [])
  opts = { echo: true, raw: false }.merge(options)
  char = console.get_char(opts)
  return if char.nil?
  codes << char.ord

  condition = proc { |escape|
    (codes - escape).empty? ||
    (escape - codes).empty? &&
    !(64..126).include?(codes.last)
  }

  while console.escape_codes.any?(&condition)
    get_codes(options, codes)
  end
  codes
end
history_next() click to toggle source
# File lib/tty/prompt/reader.rb, line 285
def history_next
  @history.next
  @history.get
end
history_next?() click to toggle source
# File lib/tty/prompt/reader.rb, line 281
def history_next?
  @history.next?
end
history_previous() click to toggle source
# File lib/tty/prompt/reader.rb, line 294
def history_previous
  line = @history.get
  @history.previous
  line
end
history_previous?() click to toggle source
# File lib/tty/prompt/reader.rb, line 290
def history_previous?
  @history.previous?
end
inspect() click to toggle source

Inspect class name and public attributes @return [String]

@api public

# File lib/tty/prompt/reader.rb, line 304
def inspect
  "#<#{self.class}: @input=#{input}, @output=#{output}>"
end
keyctrl_d(*) click to toggle source

Capture Ctrl+d and Ctrl+z key events

@api private

# File lib/tty/prompt/reader.rb, line 272
def keyctrl_d(*)
  @stop = true
end
Also aliased as: keyctrl_z
keyctrl_z(*)
Alias for: keyctrl_d
read_char(options = {})
Alias for: read_keypress
read_keypress(options = {}) click to toggle source

Read a keypress including invisible multibyte codes and return a character as a string. Nothing is echoed to the console. This call will block for a single keypress, but will not wait for Enter to be pressed.

@param [Hash] options @option options [Boolean] echo

whether to echo chars back or not, defaults to false

@option options [Boolean] raw

whenther raw mode enabled, defaults to true

@return [String]

@api public

# File lib/tty/prompt/reader.rb, line 116
def read_keypress(options = {})
  opts  = { echo: false, raw: true }.merge(options)
  codes = unbufferred { get_codes(opts) }
  char  = codes ? codes.pack('U*') : nil

  trigger_key_event(char) if char
  handle_interrupt if char == console.keys[:ctrl_c]
  char
end
Also aliased as: read_char
read_line(*args) click to toggle source

Get a single line from STDIN. Each key pressed is echoed back to the shell. The input terminates when enter or return key is pressed.

@param [String] prompt

the prompt to display before input

@param [Boolean] echo

if true echo back characters, output nothing otherwise

@return [String]

@api public

# File lib/tty/prompt/reader.rb, line 166
def read_line(*args)
  options = args.last.respond_to?(:to_hash) ? args.pop : {}
  prompt = args.empty? ? '' : args.pop
  opts = { echo: true, raw: true }.merge(options)
  line = Line.new('')
  ctrls = console.keys.keys.grep(/ctrl/)
  clear_line = "\e[2K\e[1G"

  while (codes = unbufferred { get_codes(opts) }) && (code = codes[0])
    char = codes.pack('U*')
    trigger_key_event(char)

    if console.keys[:backspace] == char || BACKSPACE == code
      next if line.start?
      line.left
      line.delete
    elsif console.keys[:delete] == char || DELETE == code
      line.delete
    elsif [console.keys[:ctrl_d],
           console.keys[:ctrl_z]].include?(char)
      break
    elsif console.keys[:ctrl_c] == char
      handle_interrupt
    elsif ctrls.include?(console.keys.key(char))
      # skip
    elsif console.keys[:up] == char
      next unless history_previous?
      line.replace(history_previous)
    elsif console.keys[:down] == char
      line.replace(history_next? ? history_next : '')
    elsif console.keys[:left] == char
      line.left
    elsif console.keys[:right] == char
      line.right
    else
      if opts[:raw] && code == CARRIAGE_RETURN
        char = "\n"
        line.move_to_end
      end
      line.insert(char)
    end

    if opts[:raw] && opts[:echo]
      output.print(clear_line)
      output.print(prompt + line.to_s)
      if char == "\n"
        line.move_to_start
      elsif !line.end?
        output.print("\e[#{line.size - line.cursor}D")
      end
    end

    break if (code == CARRIAGE_RETURN || code == NEWLINE)

    if (console.keys[:backspace] == char || BACKSPACE == code) && opts[:echo]
      if opts[:raw]
        output.print("\e[1X") unless line.start?
      else
        output.print(?\s + (line.start? ? '' :  ?\b))
      end
    end
  end
  add_to_history(line.to_s.rstrip) if track_history?
  line.to_s
end
read_lines(prompt = '')
Alias for: read_multiline
read_multiline(prompt = '') { |line| ... } click to toggle source

Read multiple lines and return them in an array. Skip empty lines in the returned lines array. The input gathering is terminated by Ctrl+d or Ctrl+z.

@param [String] prompt

the prompt displayed before the input

@yield [String] line

@return [Array]

@api public

# File lib/tty/prompt/reader.rb, line 244
def read_multiline(prompt = '')
  @stop = false
  lines = []
  loop do
    line = read_line(prompt)
    break if !line || line == ''
    next  if line !~ /\S/ && !@stop
    if block_given?
      yield(line) unless line.to_s.empty?
    else
      lines << line unless line.to_s.empty?
    end
    break if @stop
  end
  lines
end
Also aliased as: read_lines
select_console(input) click to toggle source

Select appropriate console

@api private

# File lib/tty/prompt/reader.rb, line 77
def select_console(input)
  if windows? && !env['TTY_TEST']
    WinConsole.new(input)
  else
    Console.new(input)
  end
end
trigger(event, *args) click to toggle source

Expose event broadcasting

@api public

# File lib/tty/prompt/reader.rb, line 265
def trigger(event, *args)
  publish(event, *args)
end
unbufferred(&block) click to toggle source

Get input in unbuffered mode.

@example

unbufferred do
  ...
end

@api public

# File lib/tty/prompt/reader.rb, line 93
def unbufferred(&block)
  bufferring = output.sync
  # Immediately flush output
  output.sync = true
  block[] if block_given?
ensure
  output.sync = bufferring
end

Private Instance Methods

handle_interrupt() click to toggle source

Handle input interrupt based on provided value

@api private

# File lib/tty/prompt/reader.rb, line 327
def handle_interrupt
  case @interrupt
  when :signal
    Process.kill('SIGINT', Process.pid)
  when :exit
    exit(130)
  when Proc
    @interrupt.call
  when :noop
    return
  else
    raise InputInterrupt
  end
end
trigger_key_event(char) click to toggle source

Publish event

@param [String] char

the key pressed

@return [nil]

@api private

# File lib/tty/prompt/reader.rb, line 318
def trigger_key_event(char)
  event = KeyEvent.from(console.keys, char)
  trigger(:"key#{event.key.name}", event) if event.trigger?
  trigger(:keypress, event)
end
windows?() click to toggle source

Check if Windowz mode

@return [Boolean]

@api public

# File lib/tty/prompt/reader.rb, line 347
def windows?
  ::File::ALT_SEPARATOR == '\\'
end