# coding: utf-8
# highline.rb
#
# Created by James Edward Gray II on 2005-04-26.
# Copyright 2005 Gray Productions. All rights reserved.
#
# See HighLine for documentation.
#
# This is Free Software. See LICENSE and COPYING for details.
require "erb"
require "optparse"
require "stringio"
require "abbrev"
require "highline/terminal"
require "highline/question"
require "highline/menu"
require "highline/color_scheme"
require "highline/style"
require "highline/version"
require "highline/statement"
require "highline/list_renderer"
require "highline/builtin_styles"
#
# A HighLine object is a "high-level line oriented" shell over an input and an
# output stream. HighLine simplifies common console interaction, effectively
# replacing puts() and gets(). User code can simply specify the question to ask
# and any details about user interaction, then leave the rest of the work to
# HighLine. When HighLine.ask() returns, you'll have the answer you requested,
# even if HighLine had to ask many times, validate results, perform range
# checking, convert types, etc.
#
class HighLine
include BuiltinStyles
# An internal HighLine error. User code does not need to trap this.
class QuestionError < StandardError
# do nothing, just creating a unique error type
end
class NotValidQuestionError < QuestionError
# do nothing, just creating a unique error type
end
class NotInRangeQuestionError < QuestionError
# do nothing, just creating a unique error type
end
class NoConfirmationQuestionError < QuestionError
# do nothing, just creating a unique error type
end
# The setting used to disable color output.
@@use_color = true
# Pass +false+ to _setting_ to turn off HighLine's color escapes.
def self.use_color=( setting )
@@use_color = setting
end
# Returns true if HighLine is currently using color escapes.
def self.use_color?
@@use_color
end
# For checking if the current version of HighLine supports RGB colors
# Usage: HighLine.supports_rgb_color? rescue false # rescue for compatibility with older versions
# Note: color usage also depends on HighLine.use_color being set
def self.supports_rgb_color?
true
end
# The setting used to disable EOF tracking.
@@track_eof = true
# Pass +false+ to _setting_ to turn off HighLine's EOF tracking.
def self.track_eof=( setting )
@@track_eof = setting
end
# Returns true if HighLine is currently tracking EOF for input.
def self.track_eof?
@@track_eof
end
def track_eof?
self.class.track_eof?
end
# The setting used to control color schemes.
@@color_scheme = nil
# Pass ColorScheme to _setting_ to set a HighLine color scheme.
def self.color_scheme=( setting )
@@color_scheme = setting
end
# Returns the current color scheme.
def self.color_scheme
@@color_scheme
end
# Returns +true+ if HighLine is currently using a color scheme.
def self.using_color_scheme?
not @@color_scheme.nil?
end
# Reset HighLine to default.
# Clears Style index and reset color scheme.
def self.reset
Style.clear_index
reset_color_scheme
end
def self.reset_color_scheme
self.color_scheme = nil
end
#
# Create an instance of HighLine, connected to the streams _input_
# and _output_.
#
def initialize( input = $stdin, output = $stdout,
wrap_at = nil, page_at = nil, indent_size=3, indent_level=0 )
@input = input
@output = output
@multi_indent = true
@indent_size = indent_size
@indent_level = indent_level
self.wrap_at = wrap_at
self.page_at = page_at
@question = nil
@header = nil
@prompt = nil
@key = nil
@terminal = HighLine::Terminal.get_terminal
end
# The current column setting for wrapping output.
attr_reader :wrap_at
# The current row setting for paging output.
attr_reader :page_at
# Indentation over multiple lines
attr_accessor :multi_indent
# The indentation size
attr_accessor :indent_size
# The indentation level
attr_accessor :indent_level
attr_reader :input, :output
attr_reader :key
attr_reader :question
# System specific that responds to #initialize_system_extensions,
# #terminal_size, #raw_no_echo_mode, #restore_mode, #get_character.
# It polymorphically handles specific cases for different platforms.
attr_reader :terminal
#
# A shortcut to HighLine.ask() a question that only accepts "yes" or "no"
# answers ("y" and "n" are allowed) and returns +true+ or +false+
# (+true+ for "yes"). If provided a +true+ value, _character_ will cause
# HighLine to fetch a single character response. A block can be provided
# to further configure the question as in HighLine.ask()
#
# Raises EOFError if input is exhausted.
#
def agree( yes_or_no_question, character = nil )
ask(yes_or_no_question, lambda { |yn| yn.downcase[0] == ?y}) do |q|
q.validate = /\Ay(?:es)?|no?\Z/i
q.responses[:not_valid] = 'Please enter "yes" or "no".'
q.responses[:ask_on_error] = :question
q.character = character
yield q if block_given?
end
end
#
# This method is the primary interface for user input. Just provide a
# _question_ to ask the user, the _answer_type_ you want returned, and
# optionally a code block setting up details of how you want the question
# handled. See HighLine.say() for details on the format of _question_, and
# HighLine::Question for more information about _answer_type_ and what's
# valid in the code block.
#
# If @question is set before ask() is called, parameters are
# ignored and that object (must be a HighLine::Question) is used to drive
# the process instead.
#
# Raises EOFError if input is exhausted.
#
def ask(template_or_question, answer_type = nil, options = {}, &details) # :yields: question
if template_or_question.is_a? Question
@question = template_or_question
else
@question = Question.new(template_or_question, answer_type, &details)
end
return question.ask_at(self)
end
#
# This method is HighLine's menu handler. For simple usage, you can just
# pass all the menu items you wish to display. At that point, choose() will
# build and display a menu, walk the user through selection, and return
# their choice among the provided items. You might use this in a case
# statement for quick and dirty menus.
#
# However, choose() is capable of much more. If provided, a block will be
# passed a HighLine::Menu object to configure. Using this method, you can
# customize all the details of menu handling from index display, to building
# a complete shell-like menuing system. See HighLine::Menu for all the
# methods it responds to.
#
# Raises EOFError if input is exhausted.
#
def choose( *items, &details )
menu = Menu.new(&details)
menu.choices(*items) unless items.empty?
# Set auto-completion
menu.completion = menu.options
shell_style_lambda = lambda do |command| # shell-style selection
first_word = command.to_s.split.first || ""
options = menu.options
options.extend(OptionParser::Completion)
answer = options.complete(first_word)
raise Question::NoAutoCompleteMatch unless answer
[answer.last, command.sub(/^\s*#{first_word}\s*/, "")]
end
# Set _answer_type_ so we can double as the Question for ask().
# menu.option = normal menu selection, by index or name
menu.answer_type = menu.shell ? shell_style_lambda : menu.options
selected = ask(menu)
if menu.shell
menu.select(self, *selected)
else
menu.select(self, selected)
end
end
#
# This method provides easy access to ANSI color sequences, without the user
# needing to remember to CLEAR at the end of each sequence. Just pass the
# _string_ to color, followed by a list of _colors_ you would like it to be
# affected by. The _colors_ can be HighLine class constants, or symbols
# (:blue for BLUE, for example). A CLEAR will automatically be embedded to
# the end of the returned String.
#
# This method returns the original _string_ unchanged if HighLine::use_color?
# is +false+.
#
def self.color( string, *colors )
return string unless self.use_color?
Style(*colors).color(string)
end
# In case you just want the color code, without the embedding and the CLEAR
def self.color_code(*colors)
Style(*colors).code
end
# Works as an instance method, same as the class method
def color_code(*colors)
self.class.color_code(*colors)
end
# Works as an instance method, same as the class method
def color(*args)
self.class.color(*args)
end
# Remove color codes from a string
def self.uncolor(string)
Style.uncolor(string)
end
# Works as an instance method, same as the class method
def uncolor(string)
self.class.uncolor(string)
end
def list(items, mode = :rows, option = nil)
ListRenderer.new(items, mode, option, self).render
end
#
# The basic output method for HighLine objects. If the provided _statement_
# ends with a space or tab character, a newline will not be appended (output
# will be flush()ed). All other cases are passed straight to Kernel.puts().
#
# The _statement_ argument is processed as an ERb template, supporting
# embedded Ruby code. The template is evaluated within a HighLine
# instance's binding for providing easy access to the ANSI color constants
# and the HighLine#color() method.
#
def say(statement)
statement = render_statement(statement)
return if statement.empty?
statement = (indentation+statement)
# Don't add a newline if statement ends with whitespace, OR
# if statement ends with whitespace before a color escape code.
if /[ \t](\e\[\d+(;\d+)*m)?\Z/ =~ statement
output.print(statement)
output.flush
else
output.puts(statement)
end
end
def render_statement(statement)
Statement.new(statement, self).to_s
end
#
# Set to an integer value to cause HighLine to wrap output lines at the
# indicated character limit. When +nil+, the default, no wrapping occurs. If
# set to :auto, HighLine will attempt to determine the columns
# available for the @output or use a sensible default.
#
def wrap_at=( setting )
@wrap_at = setting == :auto ? output_cols : setting
end
#
# Set to an integer value to cause HighLine to page output lines over the
# indicated line limit. When +nil+, the default, no paging occurs. If
# set to :auto, HighLine will attempt to determine the rows available
# for the @output or use a sensible default.
#
def page_at=( setting )
@page_at = setting == :auto ? output_rows - 2 : setting
end
#
# Outputs indentation with current settings
#
def indentation
' '*@indent_size*@indent_level
end
#
# Executes block or outputs statement with indentation
#
def indent(increase=1, statement=nil, multiline=nil)
@indent_level += increase
multi = @multi_indent
@multi_indent ||= multiline
begin
if block_given?
yield self
else
say(statement)
end
ensure
@multi_indent = multi
@indent_level -= increase
end
end
#
# Outputs newline
#
def newline
@output.puts
end
#
# Returns the number of columns for the console, or a default it they cannot
# be determined.
#
def output_cols
return 80 unless @output.tty?
terminal.terminal_size.first
rescue
return 80
end
#
# Returns the number of rows for the console, or a default if they cannot be
# determined.
#
def output_rows
return 24 unless @output.tty?
terminal.terminal_size.last
rescue
return 24
end
def puts(*args)
@output.puts(*args)
end
#
# Creates a new HighLine instance with the same options
#
def new_scope
self.class.new(@input, @output, @wrap_at, @page_at, @indent_size, @indent_level)
end
private
#
# A helper method for sending the output stream and error and repeat
# of the question.
#
def explain_error(error, question)
say(question.responses[error]) unless error.nil?
say(question.ask_on_error_msg)
end
#
# Gets one answer, as opposed to HighLine#gather
#
def ask_once(question)
# readline() needs to handle its own output, but readline only supports
# full line reading. Therefore if question.echo is anything but true,
# the prompt will not be issued. And we have to account for that now.
# Also, JRuby-1.7's ConsoleReader.readLine() needs to be passed the prompt
# to handle line editing properly.
say(question) unless ((question.readline) and (question.echo == true and question.limit.nil?))
begin
question.get_response_or_default(self)
raise NotValidQuestionError unless question.valid_answer?
question.convert
if question.confirm
# need to add a layer of scope (new_scope) to ask a question inside a
# question, without destroying instance data
raise NoConfirmationQuestionError unless confirm(question)
end
rescue NoConfirmationQuestionError
explain_error(nil, question)
retry
rescue NotInRangeQuestionError
explain_error(:not_in_range, question)
retry
rescue NotValidQuestionError
explain_error(:not_valid, question)
retry
rescue QuestionError
retry
rescue ArgumentError => error
case error.message
when /ambiguous/
# the assumption here is that OptionParser::Completion#complete
# (used for ambiguity resolution) throws exceptions containing
# the word 'ambiguous' whenever resolution fails
explain_error(:ambiguous_completion, question)
retry
when /invalid value for/
explain_error(:invalid_type, question)
retry
else
raise
end
rescue Question::NoAutoCompleteMatch
explain_error(:no_completion, question)
retry
end
question.answer
end
def confirm(question)
new_scope.agree(question.confirm_question(self))
end
public :ask_once
#
# Collects an Array/Hash full of answers as described in
# HighLine::Question.gather().
#
# Raises EOFError if input is exhausted.
#
def gather(question)
original_question_template = question.template
verify_match = question.verify_match
begin # when verify_match is set this loop will repeat until unique_answers == 1
question.template = original_question_template
answers =
case question.gather
when Integer
gather_integer(question)
when ::String, Regexp
gather_regexp(question)
when Hash
gather_hash(question)
end
if verify_match && (unique_answers(answers).size > 1)
explain_error(:mismatch, question)
else
verify_match = false
end
end while verify_match
question.verify_match ? last_answer(answers) : answers
end
public :gather
def gather_integer(question)
answers = []
answers << ask_once(question)
question.template = ""
(question.gather-1).times do
answers << ask_once(question)
end
answers
end
def gather_regexp(question)
answers = []
answers << ask_once(question)
question.template = ""
until (question.gather.is_a?(::String) and answers.last.to_s == question.gather) or
(question.gather.is_a?(Regexp) and answers.last.to_s =~ question.gather)
answers << ask_once(question)
end
answers.pop
answers
end
def gather_hash(question)
answers = {}
question.gather.keys.sort.each do |key|
@key = key
answers[key] = ask_once(question)
end
answers
end
#
# A helper method used by HighLine::Question.verify_match
# for finding whether a list of answers match or differ
# from each other.
#
def unique_answers(list)
(list.respond_to?(:values) ? list.values : list).uniq
end
def last_answer(answers)
answers.respond_to?(:values) ? answers.values.last : answers.last
end
#
# Read a line of input from the input stream and process whitespace as
# requested by the Question object.
#
# If Question's _readline_ property is set, that library will be used to
# fetch input. *WARNING*: This ignores the currently set input stream.
#
# Raises EOFError if input is exhausted.
#
def get_line(question)
terminal.get_line(question, self)
end
def get_response_line_mode(question)
if question.echo == true and question.limit.nil?
get_line(question)
else
line = ""
terminal.raw_no_echo_mode_exec do
while character = terminal.get_character(@input)
break if character == "\n" or character == "\r"
# honor backspace and delete
if character == "\b"
chopped = line.chop!
output_erase_char if chopped and question.echo
else
line << character
@output.print(line[-1]) if question.echo == true
@output.print(question.echo) if question.echo and question.echo != true
end
@output.flush
break if question.limit and line.size == question.limit
end
end
if question.overwrite
@output.print("\r#{HighLine.Style(:erase_line).code}")
@output.flush
else
say("\n")
end
question.format_answer(line)
end
end
def output_erase_char
@output.print("\b#{HighLine.Style(:erase_char).code}")
end
def get_response_getc_mode(question)
terminal.raw_no_echo_mode_exec do
response = @input.getc
question.format_answer(response)
end
end
def get_response_character_mode(question)
terminal.raw_no_echo_mode_exec do
response = terminal.get_character(@input)
if question.overwrite
erase_current_line
else
echo = get_echo(question, response)
say("#{echo}\n")
end
question.format_answer(response)
end
end
def erase_current_line
@output.print("\r#{HighLine.Style(:erase_line).code}")
@output.flush
end
def get_echo(question, response)
if question.echo == true
response
elsif question.echo != false
question.echo
else
""
end
end
public :get_response_character_mode, :get_response_line_mode
public :get_response_getc_mode
def actual_length(text)
Wrapper.actual_length text
end
end
require "highline/string_extensions"