class TTY::Reader

A class responsible for reading character input from STDIN

Used internally to provide key and line reading functionality

@api public

Constants

BACKSPACE
CARRIAGE_RETURN

Key codes

DELETE
EXIT_KEYS

Keys that terminate input

InputInterrupt

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

@api public

NEWLINE
VERSION

Attributes

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

Public Class Methods

new(input: $stdin, output: $stdout, interrupt: :error, env: ENV, track_history: true, history_cycle: false, history_exclude: History::DEFAULT_EXCLUDE, history_duplicates: false) click to toggle source

Initialize a Reader

@param [IO] input

the input stream

@param [IO] output

the output stream

@param [Symbol] interrupt

the way to handle the Ctrl+C key out of :signal, :exit, :noop

@param [Hash] env

the environment variables

@param [Boolean] #track_history

disable line history tracking, true by default

@param [Boolean] history_cycle

allow cycling through history, false by default

@param [Boolean] history_duplicates

allow duplicate entires, false by default

@param [Proc] history_exclude

exclude lines from history, by default all lines are stored

@api public

# File lib/tty/reader.rb, line 78
def initialize(input: $stdin, output: $stdout, interrupt: :error,
               env: ENV, track_history: true, history_cycle: false,
               history_exclude: History::DEFAULT_EXCLUDE,
               history_duplicates: false)
  @input = input
  @output = output
  @interrupt = interrupt
  @env = env
  @track_history = track_history
  @history_cycle = history_cycle
  @history_exclude = history_exclude
  @history_duplicates = history_duplicates

  @console = select_console(input)
  @history = History.new do |h|
    h.cycle = history_cycle
    h.duplicates = history_duplicates
    h.exclude = history_exclude
  end
  @stop = false # gathering input
  @cursor = TTY::Cursor

  subscribe(self)
end
windows?() click to toggle source

Check if Windowz mode

@return [Boolean]

@api public

# File lib/tty/reader.rb, line 41
def self.windows?
  ::File::ALT_SEPARATOR == "\\"
end

Public Instance Methods

add_to_history(line) click to toggle source
# File lib/tty/reader.rb, line 432
def add_to_history(line)
  @history.push(line)
end
clear_display(line, screen_width) click to toggle source

Clear display for the current line input

Handles clearing input that is longer than the current terminal width which allows copy & pasting long strings.

@param [Line] line

the line to display

@param [Number] screen_width

the terminal screen width

@api private

# File lib/tty/reader.rb, line 342
def clear_display(line, screen_width)
  total_lines  = count_screen_lines(line.size, screen_width)
  current_line = count_screen_lines(line.prompt_size + line.cursor, screen_width)
  lines_down = total_lines - current_line

  output.print(cursor.down(lines_down)) unless lines_down.zero?
  output.print(cursor.clear_lines(total_lines))
end
count_screen_lines(line_or_size, screen_width = TTY::Screen.width) click to toggle source

Count the number of screen lines given line takes up in terminal

@param [Integer] line_or_size

the current line or its length

@param [Integer] screen_width

the width of terminal screen

@return [Integer]

@api public

# File lib/tty/reader.rb, line 361
def count_screen_lines(line_or_size, screen_width = TTY::Screen.width)
  line_size = if line_or_size.is_a?(Integer)
                line_or_size
              else
                Line.sanitize(line_or_size).size
              end
  # new character + we don't want to add new line on screen_width
  new_chars = self.class.windows? ? -1 : 1
  1 + [0, (line_size - new_chars) / screen_width].max
end
get_codes(echo: true, raw: false, nonblock: false, codes: []) click to toggle source

Get input code points

@param [Boolean] echo

whether to echo chars back or not, defaults to false

@option [Boolean] raw

whenther raw mode is enabled, defaults to true

@option [Boolean] nonblock

whether to wait for input or not, defaults to false

@param [Array] codes

the currently read char code points

@return [Array]

@api private

# File lib/tty/reader.rb, line 208
def get_codes(echo: true, raw: false, nonblock: false, codes: [])
  char = console.get_char(echo: echo, raw: raw, nonblock: nonblock)
  handle_interrupt if console.keys[char] == :ctrl_c
  return if char.nil?

  codes << char.ord
  condition = proc { |escape|
    (codes - escape).empty? ||
    (escape - codes).empty? &&
    !(64..126).cover?(codes.last)
  }

  while console.escape_codes.any?(&condition)
    char_codes = get_codes(echo: echo, raw: raw,
                           nonblock: true, codes: codes)
    break if char_codes.nil?
  end

  codes
end
history_next() click to toggle source
# File lib/tty/reader.rb, line 440
def history_next
  @history.next
  @history.get
end
history_next?() click to toggle source
# File lib/tty/reader.rb, line 436
def history_next?
  @history.next?
end
history_previous() click to toggle source
# File lib/tty/reader.rb, line 449
def history_previous
  line = @history.get
  @history.previous
  line
end
history_previous?() click to toggle source
# File lib/tty/reader.rb, line 445
def history_previous?
  @history.previous?
end
inspect() click to toggle source

Inspect class name and public attributes @return [String]

@api public

# File lib/tty/reader.rb, line 459
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/reader.rb, line 427
def keyctrl_d(*)
  @stop = true
end
Also aliased as: keyctrl_z
keyctrl_z(*)
Alias for: keyctrl_d
old_subcribe(listener, options = {})
Alias for: subscribe
read_char(echo: false, raw: true, nonblock: false)
Alias for: read_keypress
read_keypress(echo: false, raw: true, nonblock: false) 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 [Boolean] echo

whether to echo chars back or not, defaults to false

@option [Boolean] raw

whenther raw mode is enabled, defaults to true

@option [Boolean] nonblock

whether to wait for input or not, defaults to false

@return [String]

@api public

# File lib/tty/reader.rb, line 183
def read_keypress(echo: false, raw: true, nonblock: false)
  codes = unbufferred do
    get_codes(echo: echo, raw: raw, nonblock: nonblock)
  end
  char = codes ? codes.pack("U*") : nil

  trigger_key_event(char) if char
  char
end
Also aliased as: read_char
read_line(prompt = "", value: "", echo: true, raw: true, nonblock: false) 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 [String] value

the value to pre-populate line with

@param [Boolean] echo

whether to echo chars back or not, defaults to false

@option [Boolean] raw

whenther raw mode is enabled, defaults to true

@option [Boolean] nonblock

whether to wait for input or not, defaults to false

@return [String]

@api public

# File lib/tty/reader.rb, line 247
def read_line(prompt = "", value: "", echo: true, raw: true, nonblock: false)
  line = Line.new(value, prompt: prompt)
  screen_width = TTY::Screen.width
  buffer = ""

  output.print(line)

  while (codes = get_codes(echo: echo, raw: raw, nonblock: nonblock)) &&
        (code = codes[0])
    char = codes.pack("U*")

    if EXIT_KEYS.include?(console.keys[char])
      trigger_key_event(char, line: line.to_s)
      break
    end

    if raw && echo
      clear_display(line, screen_width)
    end

    if console.keys[char] == :backspace || code == BACKSPACE
      if !line.start?
        line.left
        line.delete
      end
    elsif console.keys[char] == :delete || code == DELETE
      line.delete
    elsif console.keys[char].to_s =~ /ctrl_/
      # skip
    elsif console.keys[char] == :up
      line.replace(history_previous) if history_previous?
    elsif console.keys[char] == :down
      line.replace(history_next? ? history_next : buffer) if track_history?
    elsif console.keys[char] == :left
      line.left
    elsif console.keys[char] == :right
      line.right
    elsif console.keys[char] == :home
      line.move_to_start
    elsif console.keys[char] == :end
      line.move_to_end
    else
      if raw && code == CARRIAGE_RETURN
        char = "\n"
        line.move_to_end
      end
      line.insert(char)
      buffer = line.text
    end

    if (console.keys[char] == :backspace || code == BACKSPACE) && echo
      if raw
        output.print("\e[1X") unless line.start?
      else
        output.print(?\s + (line.start? ? "" : ?\b))
      end
    end

    # trigger before line is printed to allow for line changes
    trigger_key_event(char, line: line.to_s)

    if raw && echo
      output.print(line.to_s)
      if char == "\n"
        line.move_to_start
      elsif !line.end? # readjust cursor position
        output.print(cursor.backward(line.text_size - line.cursor))
      end
    end

    if [CARRIAGE_RETURN, NEWLINE].include?(code)
      buffer = ""
      output.puts unless echo
      break
    end
  end

  if track_history? && echo
    add_to_history(line.text.rstrip)
  end

  line.text
end
read_lines(prompt = "", value: "", echo: true, raw: true, nonblock: false)
Alias for: read_multiline
read_multiline(prompt = "", value: "", echo: true, raw: true, nonblock: false) { |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

@param [String] value

the value to pre-populate line with

@param [Boolean] echo

whether to echo chars back or not, defaults to false

@option [Boolean] raw

whenther raw mode is enabled, defaults to true

@option [Boolean] nonblock

whether to wait for input or not, defaults to false

@yield [String] line

@return [Array]

@api public

# File lib/tty/reader.rb, line 392
def read_multiline(prompt = "", value: "", echo: true, raw: true,
                   nonblock: false)
  @stop = false
  lines = []
  empty_str = ""

  loop do
    line = read_line(prompt, value: value, echo: echo, raw: raw,
                             nonblock: nonblock)
    value = empty_str unless value.empty? # reset
    break if !line || line == empty_str
    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/reader.rb, line 143
def select_console(input)
  if self.class.windows? && !env["TTY_TEST"]
    WinConsole.new(input)
  else
    Console.new(input)
  end
end
subscribe(listener, options = {}) { || ... } click to toggle source

Subscribe to receive key events

@example

reader.subscribe(MyListener.new)

@return [self|yield]

@api public

# File lib/tty/reader.rb, line 113
def subscribe(listener, options = {})
  old_subcribe(listener, options)
  object = self
  if block_given?
    object = yield
    unsubscribe(listener)
  end
  object
end
Also aliased as: old_subcribe
trigger(event, *args) click to toggle source

Expose event broadcasting

@api public

# File lib/tty/reader.rb, line 420
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/reader.rb, line 159
def unbufferred(&block)
  bufferring = output.sync
  # Immediately flush output
  output.sync = true
  block[] if block_given?
ensure
  output.sync = bufferring
end
unsubscribe(listener) click to toggle source

Unsubscribe from receiving key events

@example

reader.unsubscribe(my_listener)

@return [void]

@api public

# File lib/tty/reader.rb, line 131
def unsubscribe(listener)
  registry = send(:local_registrations)
  registry.each do |object|
    if object.listener.equal?(listener)
      registry.delete(object)
    end
  end
end

Private Instance Methods

handle_interrupt() click to toggle source

Handle input interrupt based on provided value

@api private

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

Publish event

@param [String] char

the key pressed

@return [nil]

@api private

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