module Interactive

Copyright © 2012 Alex Suraci

Constants

ESCAPES
EVENTS

Public Instance Methods

ask(question, options = {}) click to toggle source

Ask a question and get an answer.

See Interact#read_line for the other possible values in options.

question

The prompt, without “: ” at the end.

options

An optional hash containing the following options.

default

The default value, also used to attempt type conversion of the answer (e.g. numeric/boolean).

choices

An array (or Enumerable) of strings to choose from.

indexed

Use alternative choice listing, and allow choosing by number. Good for when there are many choices or choices with long names.

# File lib/interact/interactive.rb, line 172
def ask(question, options = {})
  choices = options[:choices] && options[:choices].to_a

  list_choices(choices, options) if choices

  while true
    prompt(question, options)
    ok, res = answered(read_line(options), options)
    return res if ok
  end
end
read_char(options = {}) click to toggle source

Read a single character.

options

An optional hash containing the following options.

input

The input source (defaults to $stdin).

# File lib/interact/interactive.rb, line 109
def read_char(options = {})
  input = options[:input] || $stdin

  with_char_io(input) do
    get_character(input)
  end
end
read_event(options = {}) click to toggle source

Read a single event.

options

An optional hash containing the following options.

input

The input source (defaults to $stdin).

# File lib/interact/interactive.rb, line 123
def read_event(options = {})
  input = options[:input] || $stdin

  with_char_io(input) do
    get_event(input)
  end
end
read_line(options = {}) click to toggle source

Read a line of input.

options

An optional hash containing the following options.

input

The input source (defaults to $stdin).

echo

A string to echo when showing the input; used for things like hiding password input.

# File lib/interact/interactive.rb, line 141
def read_line(options = {})
  input = options[:input] || $stdin

  state = input_state(options)
  with_char_io(input) do
    until state.done?
      handler(get_event(input), state)
    end
  end

  state.answer
end

Private Instance Methods

answered(ans, options) click to toggle source
# File lib/interact/interactive.rb, line 240
def answered(ans, options)
  print "\n"

  if ans.empty?
    if options.key?(:default)
      [true, options[:default]]
    end
  elsif choices = options[:choices]
    matches = []

    choices.each do |x|
      completion = choice_completion(x, options)

      if completion == ans
        return [true, x]
      elsif completion.start_with? ans
        matches << x
      end
    end

    if choices and ans =~ /^\s*\d+\s*$/ and            ans.to_i - 1 >= 0 and res = choices.to_a[ans.to_i - 1]
      [true, res]
    elsif matches.size == 1
      [true, matches.first]
    elsif matches.size > 1
      matches_list = matches.collect { |m|
        show_choice(m, options)
      }.join " or "

      puts "Please disambiguate: #{matches_list}?"

      [false, nil]
    elsif options[:allow_other]
      [true, ans]
    else
      puts "Unknown answer, please try again!"
      [false, nil]
    end
  else
    [true, match_type(ans, options[:default])]
  end
end
choice_completion(choice, options = {}) click to toggle source
# File lib/interact/interactive.rb, line 297
def choice_completion(choice, options = {})
  complete = options[:complete] || options[:display] || proc(&:to_s)
  complete.call(choice)
end
chr(x) click to toggle source
# File lib/interact/interactive.rb, line 481
def chr(x)
  case x
  when nil
    nil
  when String
    x
  else
    x.chr
  end
end
clear_input(state) click to toggle source
# File lib/interact/interactive.rb, line 186
def clear_input(state)
  state.goto(0)
  state.clear(state.answer.size)
  state.answer = ""
end
common_prefix(*strs) click to toggle source
# File lib/interact/interactive.rb, line 302
def common_prefix(*strs)
  return strs.first.dup if strs.size == 1

  longest = strs.sort_by(&:size).last
  longest.size.times do |i|
    sub = longest[0..(-1 - i)]
    if strs.all? { |s| s.start_with?(sub) }
      return sub
    end
  end

  ""
end
get_character(input) click to toggle source
# File lib/interact/interactive.rb, line 506
def get_character(input)
  if input == STDIN
    begin
      chr(Win32API.new("msvcrt", "_getch", [], "L").call)
    rescue
      chr(Win32API.new("crtdll", "_getch", [], "L").call)
    end
  else
    chr(input.getc)
  end
end
get_event(input) click to toggle source
# File lib/interact/interactive.rb, line 209
def get_event(input)
  escaped = false
  escape_seq = ""

  while true
    c = get_character(input)

    if not c
      return :eof
    elsif c == "\e" || c == "\xE0"
      escaped = true
    elsif escaped
      escape_seq << c

      if cmd = ESCAPES[escape_seq]
        return cmd
      elsif ESCAPES.select { |k, v|
              k.start_with? escape_seq
            }.empty?
        escaped, escape_seq = false, ""
      end
    elsif EVENTS.key? c
      return EVENTS[c]
    elsif c < " "
      # ignore
    else
      return [:key, c]
    end
  end
end
handler(which, state) click to toggle source
# File lib/interact/interactive.rb, line 316
def handler(which, state)
  ans = state.answer
  pos = state.position

  case which
  when :up
    # nothing

  when :down
    # nothing

  when :tab
    matches =
      if choices = state.options[:choices]
        choices.collect { |c|
          choice_completion(c, state.options)
        }.select { |c|
          c.start_with? ans
        }
      else
        matching_paths(ans)
      end

    if matches.empty?
      print("\a") # bell
    else
      old = ans
      ans = state.answer = common_prefix(*matches)
      state.display(ans[pos .. -1])
      print("\a") if ans == old
    end

  when :right
    unless pos == ans.size
      state.display(ans[pos .. pos])
    end

  when :left
    unless pos == 0
      state.back(1)
    end

  when :delete
    unless pos == ans.size
      ans.slice!(pos, 1)
      rest = ans[pos .. -1]
      state.display(rest)
      state.clear(1)
      state.back(rest.size)
    end

  when :home
    state.goto(0)

  when :end
    state.goto(ans.size)

  when :backspace
    if pos > 0
      rest = ans[pos .. -1]

      ans.slice!(pos - 1, 1)

      state.back(1)
      state.display(rest)
      state.clear(1)
      state.back(rest.size)
    end

  when :interrupt
    raise Interrupt.new

  when :eof
    state.done! if ans.empty?

  when :kill_word
    if pos > 0
      start = /[[:alnum:]]*\s*[^[:alnum:]]?$/ =~ ans[0 .. (pos - 1)]

      if pos < ans.size
        to_end = ans.size - pos
        rest = ans[pos .. -1]
        state.clear(to_end)
      end

      length = pos - start

      ans.slice!(start, length)
      state.back(length)
      state.clear(length)

      if to_end
        state.display(rest)
        state.back(to_end)
      end
    end

  when :enter
    state.done!

  when Array
    case which[0]
    when :key
      c = which[1]
      rest = ans[pos .. -1]

      ans.insert(pos, c)

      state.display(c + rest)
      state.back(rest.size)
    end

  else
    return false
  end

  true
end
input_state(options) click to toggle source
# File lib/interact/interactive.rb, line 205
def input_state(options)
  InputState.new(options)
end
list_choices(choices, options = {}) click to toggle source
# File lib/interact/interactive.rb, line 284
def list_choices(choices, options = {})
  return unless options[:indexed]

  choices.each_with_index do |o, i|
    puts "#{i + 1}: #{show_choice(o, options)}"
  end
end
match_type(str, x) click to toggle source
# File lib/interact/interactive.rb, line 463
def match_type(str, x)
  case x
  when Integer
    str.to_i
  when true, false
    str.upcase.start_with? "Y"
  else
    str
  end
end
matching_paths(input) click to toggle source
# File lib/interact/interactive.rb, line 435
def matching_paths(input)
  home = File.expand_path("~")

  Dir.glob(input.sub("~", home) + "*").collect do |p|
    p.sub(home, "~")
  end
end
prompt(question, options = {}) click to toggle source
# File lib/interact/interactive.rb, line 443
def prompt(question, options = {})
  print question

  if (choices = options[:choices]) && !options[:indexed]
    print " (#{choices.collect(&:to_s).join ", "})"
  end

  case options[:default]
  when true
    print " [Yn]"
  when false
    print " [yN]"
  when nil
  else
    print " [#{options[:default]}]"
  end

  print ": "
end
redraw_input(state) click to toggle source
# File lib/interact/interactive.rb, line 198
def redraw_input(state)
  pos = state.position
  state.goto(0)
  state.display(state.answer)
  state.goto(pos)
end
restore_input_state(input, state) click to toggle source
# File lib/interact/interactive.rb, line 502
def restore_input_state(input, state)
  nil
end
set_input(state, input) click to toggle source
# File lib/interact/interactive.rb, line 192
def set_input(state, input)
  clear_input(state)
  state.display(input)
  state.answer = input
end
set_input_state(input) click to toggle source
# File lib/interact/interactive.rb, line 498
def set_input_state(input)
  nil
end
show_choice(choice, options = {}) click to toggle source
# File lib/interact/interactive.rb, line 292
def show_choice(choice, options = {})
  display = options[:display] || proc(&:to_s)
  display.call(choice)
end
with_char_io(input) { || ... } click to toggle source
# File lib/interact/interactive.rb, line 474
def with_char_io(input)
  before = set_input_state(input)
  yield
ensure
  restore_input_state(input, before)
end