class Infopark::UserIO

TODO

Constants

VERSION

Attributes

global[RW]

Public Class Methods

new(output_prefix: nil) click to toggle source
# File lib/infopark/user_io.rb, line 63
def initialize(output_prefix: nil)
  case output_prefix
  when String
    @output_prefix = "[#{output_prefix}] "
  when Proc, Method
    @output_prefix_proc = ->() { "[#{output_prefix.call}] " }
  when :timestamp
    @output_prefix_proc = ->() { "[#{Time.now.strftime("%T.%L")}] " }
  end
  @line_pending = {}
end

Public Instance Methods

<<(msg) click to toggle source
# File lib/infopark/user_io.rb, line 172
def <<(msg)
  tell(msg.chomp, newline: msg.end_with?("\n"))
end
acknowledge(*text) click to toggle source
# File lib/infopark/user_io.rb, line 110
def acknowledge(*text)
  tell("-" * 80)
  tell(*text, color: :cyan, bright: true)
  tell("-" * 80)
  tell("Please press ENTER to continue.")
  read_line
end
ask(*text, default: nil, expected: "yes") click to toggle source
# File lib/infopark/user_io.rb, line 118
def ask(*text, default: nil, expected: "yes")
  # TODO implementation error if default not boolean or nil
  # TODO implementation error if expected not "yes" or "no"
  tell("-" * 80)
  tell(*text, color: :cyan, bright: true)
  tell("-" * 80)
  default_answer = default ? "yes" : "no" unless default.nil?
  tell("(yes/no) #{default_answer && "[#{default_answer}] "}> ", newline: false)
  until %w(yes no).include?(answer = read_line.strip.downcase)
    if answer.empty?
      answer = default_answer
      break
    end
    tell("I couldn't understand “#{answer}”.", newline: false, color: :red, bright: true)
    tell(" > ", newline: false)
  end
  answer == expected
end
background_other_threads() click to toggle source
# File lib/infopark/user_io.rb, line 155
def background_other_threads
  unless @foreground_thread
    @background_data = []
    @foreground_thread = Thread.current
  end
end
confirm(*text) click to toggle source
# File lib/infopark/user_io.rb, line 143
def confirm(*text)
  ask(*text) or raise Aborted
end
edit_file(kind_of_data, filename = nil, template: nil) click to toggle source
# File lib/infopark/user_io.rb, line 180
def edit_file(kind_of_data, filename = nil, template: nil)
  wait_for_foreground if background?

  editor = ENV['EDITOR'] or raise MissingEnv, "No EDITOR specified."

  filename ||= Tempfile.new("").path
  if template && (!File.exists?(filename) || File.empty?(filename))
    File.write(filename, template)
  end

  tell("Start editing #{kind_of_data} using #{editor}…")
  sleep(1.7)
  system(editor, filename)

  File.read(filename)
end
foreground() click to toggle source
# File lib/infopark/user_io.rb, line 162
def foreground
  if @foreground_thread
    @background_data.each(&STDOUT.method(:write))
    @foreground_thread = nil
    # take over line_pending from background
    @line_pending[false] = @line_pending[true]
    @line_pending[true] = false
  end
end
listen(prompt = nil, **options) click to toggle source
# File lib/infopark/user_io.rb, line 137
def listen(prompt = nil, **options)
  prompt << " " if prompt
  tell("#{prompt}> ", **options, newline: false)
  read_line.strip
end
new_progress(label) click to toggle source
# File lib/infopark/user_io.rb, line 147
def new_progress(label)
  Progress.new(label, self)
end
select(description, items, item_describer: :to_s, default: nil) click to toggle source
# File lib/infopark/user_io.rb, line 197
def select(description, items, item_describer: :to_s, default: nil)
  return if items.empty?

  describer =
      case item_describer
      when Method, Proc
        item_describer
      else
        ->(item) { item.send(item_describer) }
      end

  choice = nil
  if items.size == 1
    choice = items.first
    tell("Selected #{describer.call(choice)}.", color: :yellow)
    return choice
  end

  items = items.sort_by(&describer)

  tell("-" * 80)
  tell("Please select #{description}:", color: :cyan, bright: true)
  items.each_with_index do |item, i|
    tell("#{i + 1}: #{describer.call(item)}", color: :cyan, bright: true)
  end
  tell("-" * 80)
  default_index = items.index(default)
  default_selection = "[#{default_index + 1}] " if default_index
  until choice
    tell("Your choice #{default_selection}> ", newline: false)
    answer = read_line.strip
    if answer.empty?
      choice = default
    else
      int_answer = answer.to_i
      if int_answer.to_s != answer
        tell("Please enter a valid integer.")
      elsif int_answer < 1 || int_answer > items.size
        tell("Please enter a number from 1 through #{items.size}.")
      else
        choice = items[int_answer - 1]
      end
    end
  end
  choice
end
start_progress(label) click to toggle source
# File lib/infopark/user_io.rb, line 151
def start_progress(label)
  new_progress(label).tap(&:start)
end
tell(*texts, newline: true, **line_options) click to toggle source
# File lib/infopark/user_io.rb, line 75
def tell(*texts, newline: true, **line_options)
  lines = texts.flatten.map {|text| text.to_s.split("\n", -1) }.flatten

  lines[0...-1].each {|line| tell_line(line, **line_options) }
  tell_line(lines.last, newline: newline, **line_options)
end
tell_error(e, **options) click to toggle source
# File lib/infopark/user_io.rb, line 105
def tell_error(e, **options)
  tell(e, **options, color: :red, bright: true)
  tell(e.backtrace, **options, color: :red) if Exception === e
end
tell_pty_stream(stream, **color_options) click to toggle source
# File lib/infopark/user_io.rb, line 82
def tell_pty_stream(stream, **color_options)
  color_prefix, color_postfix = compute_color(**color_options)
  write_raw(output_prefix) unless line_pending?
  write_raw(color_prefix)
  nl_pending = false
  uncolored_prefix = "#{color_postfix}#{output_prefix}#{color_prefix}"
  until stream.eof?
    chunk = stream.read_nonblock(100)
    next if chunk.empty?
    write_raw("\n#{uncolored_prefix}") if nl_pending
    chunk.chop! if nl_pending = chunk.end_with?("\n")
    chunk.gsub!(/([\r\n])/, "\\1#{uncolored_prefix}")
    write_raw(chunk)
  end
  write_raw("\n") if nl_pending
  write_raw(color_postfix)
  line_pending!(false)
end
tty?() click to toggle source
# File lib/infopark/user_io.rb, line 176
def tty?
  STDOUT.tty?
end
warn(*text) click to toggle source
# File lib/infopark/user_io.rb, line 101
def warn(*text)
  tell(*text, color: :yellow, bright: true)
end

Private Instance Methods

background?() click to toggle source
# File lib/infopark/user_io.rb, line 246
def background?
  !!@foreground_thread && @foreground_thread != Thread.current
end
compute_color(**options) click to toggle source
# File lib/infopark/user_io.rb, line 290
def compute_color(**options)
  if tty?
    if prefix = text_color(**options)
      # TODO matching annihilators for options
      postfix = text_color(color: :none, bright: false)
    end
  end
  [prefix, postfix]
end
control_sequence(*parameters, function) click to toggle source
# File lib/infopark/user_io.rb, line 300
def control_sequence(*parameters, function)
  "\033[#{parameters.join(";")}#{function}"
end
line_pending!(value) click to toggle source
# File lib/infopark/user_io.rb, line 286
def line_pending!(value)
  @line_pending[background?] = value
end
line_pending?() click to toggle source
# File lib/infopark/user_io.rb, line 282
def line_pending?
  @line_pending[background?]
end
output_prefix() click to toggle source
# File lib/infopark/user_io.rb, line 254
def output_prefix
  @output_prefix || @output_prefix_proc && @output_prefix_proc.call
end
read_line() click to toggle source
# File lib/infopark/user_io.rb, line 258
def read_line
  wait_for_foreground if background?
  @line_pending[false] = false
  STDIN.gets.chomp
end
sgr_sequence(*parameters) click to toggle source

SGR: Select Graphic Rendition … far too long for a function name ;)

# File lib/infopark/user_io.rb, line 305
def sgr_sequence(*parameters)
  control_sequence(*parameters, :m)
end
tell_line(line, newline: true, prefix: true, **color_options) click to toggle source
# File lib/infopark/user_io.rb, line 264
def tell_line(line, newline: true, prefix: true, **color_options)
  line_prefix, line_postfix = compute_color(**color_options)
  prefix = false if line_pending?

  out_line = "#{output_prefix if prefix}#{line_prefix}#{line}#{line_postfix}#{"\n" if newline}"
  write_raw(out_line)

  line_pending!(!newline)
end
text_color(color: nil, bright: nil, faint: nil, italic: nil, underline: nil) click to toggle source
# File lib/infopark/user_io.rb, line 309
def text_color(color: nil, bright: nil, faint: nil, italic: nil, underline: nil)
  return if color.nil? && bright.nil?
  sequence = []
  unless bright.nil? && faint.nil?
    sequence << (bright ? 1 : (faint ? 2 : 22))
  end
  unless italic.nil?
    sequence << (italic ? 3 : 23)
  end
  unless underline.nil?
    sequence << (underline ? 4 : 24)
  end
  case color
  when :red
    sequence << 31
  when :green
    sequence << 32
  when :yellow
    sequence << 33
  when :blue
    sequence << 34
  when :purple
    sequence << 35
  when :cyan
    sequence << 36
  when :white
    sequence << 37
  when :none
    sequence << 39
  end
  sgr_sequence(*sequence)
end
wait_for_foreground() click to toggle source
# File lib/infopark/user_io.rb, line 250
def wait_for_foreground
  sleep 0.1 while background?
end
write_raw(bytes) click to toggle source
# File lib/infopark/user_io.rb, line 274
def write_raw(bytes)
  if background?
    @background_data << bytes
  else
    STDOUT.write(bytes)
  end
end