# 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/system_extensions" require "highline/question" require "highline/menu" require "highline/color_scheme" require "highline/style" # # 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 # The version of the installed library. VERSION = "1.6.12".freeze # An internal HighLine error. User code does not need to trap this. class QuestionError < StandardError # 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 # The setting used to control color schemes. @@color_scheme = nil # Pass ColorScheme to _setting_ to turn 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 # # Embed in a String to clear all previous ANSI sequences. This *MUST* be # done before the program exits! # ERASE_LINE_STYLE = Style.new(:name=>:erase_line, :builtin=>true, :code=>"\e[K") # Erase the current line of terminal output ERASE_CHAR_STYLE = Style.new(:name=>:erase_char, :builtin=>true, :code=>"\e[P") # Erase the character under the cursor. CLEAR_STYLE = Style.new(:name=>:clear, :builtin=>true, :code=>"\e[0m") # Clear color settings RESET_STYLE = Style.new(:name=>:reset, :builtin=>true, :code=>"\e[0m") # Alias for CLEAR. BOLD_STYLE = Style.new(:name=>:bold, :builtin=>true, :code=>"\e[1m") # Bold; Note: bold + a color works as you'd expect, # for example bold black. Bold without a color displays # the system-defined bold color (e.g. red on Mac iTerm) DARK_STYLE = Style.new(:name=>:dark, :builtin=>true, :code=>"\e[2m") # Dark; support uncommon UNDERLINE_STYLE = Style.new(:name=>:underline, :builtin=>true, :code=>"\e[4m") # Underline UNDERSCORE_STYLE = Style.new(:name=>:underscore, :builtin=>true, :code=>"\e[4m") # Alias for UNDERLINE BLINK_STYLE = Style.new(:name=>:blink, :builtin=>true, :code=>"\e[5m") # Blink; support uncommon REVERSE_STYLE = Style.new(:name=>:reverse, :builtin=>true, :code=>"\e[7m") # Reverse foreground and background CONCEALED_STYLE = Style.new(:name=>:concealed, :builtin=>true, :code=>"\e[8m") # Concealed; support uncommon STYLES = %w{CLEAR RESET BOLD DARK UNDERLINE UNDERSCORE BLINK REVERSE CONCEALED} # These RGB colors are approximate; see http://en.wikipedia.org/wiki/ANSI_escape_code BLACK_STYLE = Style.new(:name=>:black, :builtin=>true, :code=>"\e[30m", :rgb=>[ 0, 0, 0]) RED_STYLE = Style.new(:name=>:red, :builtin=>true, :code=>"\e[31m", :rgb=>[128, 0, 0]) GREEN_STYLE = Style.new(:name=>:green, :builtin=>true, :code=>"\e[32m", :rgb=>[ 0,128, 0]) BLUE_STYLE = Style.new(:name=>:blue, :builtin=>true, :code=>"\e[34m", :rgb=>[ 0, 0,128]) YELLOW_STYLE = Style.new(:name=>:yellow, :builtin=>true, :code=>"\e[33m", :rgb=>[128,128, 0]) MAGENTA_STYLE = Style.new(:name=>:magenta, :builtin=>true, :code=>"\e[35m", :rgb=>[128, 0,128]) CYAN_STYLE = Style.new(:name=>:cyan, :builtin=>true, :code=>"\e[36m", :rgb=>[ 0,128,128]) # On Mac OSX Terminal, white is actually gray WHITE_STYLE = Style.new(:name=>:white, :builtin=>true, :code=>"\e[37m", :rgb=>[192,192,192]) # Alias for WHITE, since WHITE is actually a light gray on Macs GRAY_STYLE = Style.new(:name=>:gray, :builtin=>true, :code=>"\e[37m", :rgb=>[192,192,192]) # On Mac OSX Terminal, this is black foreground, or bright white background. # Also used as base for RGB colors, if available NONE_STYLE = Style.new(:name=>:none, :builtin=>true, :code=>"\e[38m", :rgb=>[ 0, 0, 0]) BASIC_COLORS = %w{BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE GRAY NONE} colors = BASIC_COLORS.dup BASIC_COLORS.each do |color| bright_color = "BRIGHT_#{color}" colors << bright_color const_set bright_color+'_STYLE', const_get(color + '_STYLE').bright end COLORS = colors colors.each do |color| const_set color, const_get("#{color}_STYLE").code const_set "ON_#{color}_STYLE", const_get("#{color}_STYLE").on const_set "ON_#{color}", const_get("ON_#{color}_STYLE").code end ON_NONE_STYLE.rgb = [255,255,255] # Override; white background STYLES.each do |style| const_set style, const_get("#{style}_STYLE").code end # For RGB colors: def self.const_missing(name) if name.to_s =~ /^(ON_)?(RGB_)([A-F0-9]{6})(_STYLE)?$/ # RGB color on = $1 suffix = $4 if suffix code_name = $1.to_s + $2 + $3 else code_name = name.to_s end style_name = code_name + '_STYLE' style = Style.rgb($3) style = style.on if on const_set(style_name, style) const_set(code_name, style.code) if suffix style else style.code end else raise NameError, "Bad color or uninitialized constant #{name}" end 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 ) @input = input @output = output if JRUBY require 'java' java_import 'java.io.OutputStreamWriter' java_import 'java.nio.channels.Channels' java_import 'jline.ConsoleReader' java_import 'jline.Terminal' @java_input = Channels.newInputStream($stdin.to_channel) @java_output = OutputStreamWriter.new(Channels.newOutputStream($stdout.to_channel)) @java_terminal = Terminal.getTerminal @java_console = ConsoleReader.new(@java_input, @java_output) @java_console.setUseHistory(false) @java_console.setBellEnabled(true) @java_console.setUsePagination(false) end self.wrap_at = wrap_at self.page_at = page_at @question = nil @answer = nil @menu = nil @header = nil @prompt = nil @gather = nil @answers = nil @key = nil end include HighLine::SystemExtensions # The current column setting for wrapping output. attr_reader :wrap_at # The current row setting for paging output. attr_reader :page_at # # 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( question, answer_type = String, &details ) # :yields: question @question ||= Question.new(question, answer_type, &details) return gather if @question.gather # readline() needs to handle it's 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. say(@question) unless (@question.readline and @question.echo == true) begin @answer = @question.answer_or_default(get_response) unless @question.valid_answer?(@answer) explain_error(:not_valid) raise QuestionError end @answer = @question.convert(@answer) if @question.in_range?(@answer) if @question.confirm # need to add a layer of scope to ask a question inside a # question, without destroying instance data context_change = self.class.new(@input, @output, @wrap_at, @page_at) if @question.confirm == true confirm_question = "Are you sure? " else # evaluate ERb under initial scope, so it will have # access to @question and @answer template = ERB.new(@question.confirm, nil, "%") confirm_question = template.result(binding) end unless context_change.agree(confirm_question) explain_error(nil) raise QuestionError end end @answer else explain_error(:not_in_range) raise QuestionError end rescue QuestionError retry rescue ArgumentError, NameError => error raise if error.is_a?(NoMethodError) if error.message =~ /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) else explain_error(:invalid_type) end retry rescue Question::NoAutoCompleteMatch explain_error(:no_completion) retry ensure @question = nil # Reset Question object. end 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 amoung 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 = @question = Menu.new(&details) @menu.choices(*items) unless items.empty? # Set _answer_type_ so we can double as the Question for ask(). @menu.answer_type = if @menu.shell 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) if answer.nil? raise Question::NoAutoCompleteMatch end [answer.last, command.sub(/^\s*#{first_word}\s*/, "")] end else @menu.options # normal menu selection, by index or name end # Provide hooks for ERb layouts. @header = @menu.header @prompt = @menu.prompt if @menu.shell selected = ask("Ignored", @menu.answer_type) @menu.select(self, *selected) else selected = ask("Ignored", @menu.answer_type) @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 # # This method is a utility for quickly and easily laying out lists. It can # be accessed within ERb replacements of any text that will be sent to the # user. # # The only required parameter is _items_, which should be the Array of items # to list. A specified _mode_ controls how that list is formed and _option_ # has different effects, depending on the _mode_. Recognized modes are: # # :columns_across:: _items_ will be placed in columns, # flowing from left to right. If given, # _option_ is the number of columns to be # used. When absent, columns will be # determined based on _wrap_at_ or a # default of 80 characters. # :columns_down:: Identical to :columns_across, # save flow goes down. # :uneven_columns_across:: Like :columns_across but each # column is sized independently. # :uneven_columns_down:: Like :columns_down but each # column is sized independently. # :inline:: All _items_ are placed on a single line. # The last two _items_ are separated by # _option_ or a default of " or ". All # other _items_ are separated by ", ". # :rows:: The default mode. Each of the _items_ is # placed on it's own line. The _option_ # parameter is ignored in this mode. # # Each member of the _items_ Array is passed through ERb and thus can contain # their own expansions. Color escape expansions do not contribute to the # final field width. # def list( items, mode = :rows, option = nil ) items = items.to_ary.map do |item| ERB.new(item, nil, "%").result(binding) end if items.empty? "" else case mode when :inline option = " or " if option.nil? if items.size == 1 items.first else items[0..-2].join(", ") + "#{option}#{items.last}" end when :columns_across, :columns_down max_length = actual_length( items.max { |a, b| actual_length(a) <=> actual_length(b) } ) if option.nil? limit = @wrap_at || 80 option = (limit + 2) / (max_length + 2) end items = items.map do |item| pad = max_length + (item.to_s.length - actual_length(item)) "%-#{pad}s" % item end row_count = (items.size / option.to_f).ceil if mode == :columns_across rows = Array.new(row_count) { Array.new } items.each_with_index do |item, index| rows[index / option] << item end rows.map { |row| row.join(" ") + "\n" }.join else columns = Array.new(option) { Array.new } items.each_with_index do |item, index| columns[index / row_count] << item end list = "" columns.first.size.times do |index| list << columns.map { |column| column[index] }. compact.join(" ") + "\n" end list end when :uneven_columns_across if option.nil? limit = @wrap_at || 80 items.size.downto(1) do |column_count| row_count = (items.size / column_count.to_f).ceil rows = Array.new(row_count) { Array.new } items.each_with_index do |item, index| rows[index / column_count] << item end widths = Array.new(column_count, 0) rows.each do |row| row.each_with_index do |field, column| size = actual_length(field) widths[column] = size if size > widths[column] end end if column_count == 1 or widths.inject(0) { |sum, n| sum + n + 2 } <= limit + 2 return rows.map { |row| row.zip(widths).map { |field, i| "%-#{i + (field.to_s.length - actual_length(field))}s" % field }.join(" ") + "\n" }.join end end else row_count = (items.size / option.to_f).ceil rows = Array.new(row_count) { Array.new } items.each_with_index do |item, index| rows[index / option] << item end widths = Array.new(option, 0) rows.each do |row| row.each_with_index do |field, column| size = actual_length(field) widths[column] = size if size > widths[column] end end return rows.map { |row| row.zip(widths).map { |field, i| "%-#{i + (field.to_s.length - actual_length(field))}s" % field }.join(" ") + "\n" }.join end when :uneven_columns_down if option.nil? limit = @wrap_at || 80 items.size.downto(1) do |column_count| row_count = (items.size / column_count.to_f).ceil columns = Array.new(column_count) { Array.new } items.each_with_index do |item, index| columns[index / row_count] << item end widths = Array.new(column_count, 0) columns.each_with_index do |column, i| column.each do |field| size = actual_length(field) widths[i] = size if size > widths[i] end end if column_count == 1 or widths.inject(0) { |sum, n| sum + n + 2 } <= limit + 2 list = "" columns.first.size.times do |index| list << columns.zip(widths).map { |column, width| field = column[index] "%-#{width + (field.to_s.length - actual_length(field))}s" % field }.compact.join(" ").strip + "\n" end return list end end else row_count = (items.size / option.to_f).ceil columns = Array.new(option) { Array.new } items.each_with_index do |item, index| columns[index / row_count] << item end widths = Array.new(option, 0) columns.each_with_index do |column, i| column.each do |field| size = actual_length(field) widths[i] = size if size > widths[i] end end list = "" columns.first.size.times do |index| list << columns.zip(widths).map { |column, width| field = column[index] "%-#{width + (field.to_s.length - actual_length(field))}s" % field }.compact.join(" ").strip + "\n" end return list end else items.map { |i| "#{i}\n" }.join end end 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_ parameter is processed as an ERb template, supporting # embedded Ruby code. The template is evaluated with a binding inside # the HighLine instance, providing easy access to the ANSI color constants # and the HighLine.color() method. # def say( statement ) statement = statement.to_str return unless statement.length > 0 template = ERB.new(statement, nil, "%") statement = template.result(binding) statement = wrap(statement) unless @wrap_at.nil? statement = page_print(statement) unless @page_at.nil? if statement[-1, 1] == " " or statement[-1, 1] == "\t" @output.print(statement) @output.flush else @output.puts(statement) end 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 determing 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 determing the rows available # for the @output or use a sensible default. # def page_at=( setting ) @page_at = setting == :auto ? output_rows - 2 : setting 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_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_size.last rescue return 24 end private # # A helper method for sending the output stream and error and repeat # of the question. # def explain_error( error ) say(@question.responses[error]) unless error.nil? if @question.responses[:ask_on_error] == :question say(@question) elsif @question.responses[:ask_on_error] say(@question.responses[:ask_on_error]) end end # # Collects an Array/Hash full of answers as described in # HighLine::Question.gather(). # # Raises EOFError if input is exhausted. # def gather( ) original_question = @question original_question_string = @question.question original_gather = @question.gather verify_match = @question.verify_match @question.gather = false begin # when verify_match is set this loop will repeat until unique_answers == 1 @answers = [ ] @gather = original_gather original_question.question = original_question_string case @gather when Integer @answers << ask(@question) @gather -= 1 original_question.question = "" until @gather.zero? @question = original_question @answers << ask(@question) @gather -= 1 end when ::String, Regexp @answers << ask(@question) original_question.question = "" until (@gather.is_a?(::String) and @answers.last.to_s == @gather) or (@gather.is_a?(Regexp) and @answers.last.to_s =~ @gather) @question = original_question @answers << ask(@question) end @answers.pop when Hash @answers = { } @gather.keys.sort.each do |key| @question = original_question @key = key @answers[key] = ask(@question) end end if verify_match && (unique_answers(@answers).size > 1) @question = original_question explain_error(:mismatch) else verify_match = false end end while verify_match original_question.verify_match ? @answer : @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 = @answers) (list.respond_to?(:values) ? list.values : list).uniq 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( ) if @question.readline require "readline" # load only if needed # capture say()'s work in a String to feed to readline() old_output = @output @output = StringIO.new say(@question) question = @output.string @output = old_output # prep auto-completion Readline.completion_proc = lambda do |string| @question.selection.grep(/\A#{Regexp.escape(string)}/) end # work-around ugly readline() warnings old_verbose = $VERBOSE $VERBOSE = nil raw_answer = Readline.readline(question, true) if raw_answer.nil? if @@track_eof raise EOFError, "The input stream is exhausted." else raw_answer = String.new # Never return nil end end answer = @question.change_case( @question.remove_whitespace(raw_answer)) $VERBOSE = old_verbose answer else if JRUBY enable_echo_afterwards = @java_terminal.isEchoEnabled @java_terminal.disableEcho begin raw_answer = @java_console.readLine(nil, nil) ensure @java_terminal.enableEcho if enable_echo_afterwards end else raise EOFError, "The input stream is exhausted." if @@track_eof and @input.eof? raw_answer = @input.gets end @question.change_case(@question.remove_whitespace(raw_answer)) end end def get_single_character(is_stty) if JRUBY @java_console.readVirtualKey elsif is_stty @input.getbyte else get_character(@input) end end # # Return a line or character of input, as requested for this question. # Character input will be returned as a single character String, # not an Integer. # # This question's _first_answer_ will be returned instead of input, if set. # # Raises EOFError if input is exhausted. # def get_response( ) return @question.first_answer if @question.first_answer? stty = (CHARACTER_MODE == "stty") if @question.character.nil? if @question.echo == true and @question.limit.nil? get_line else if JRUBY enable_echo_afterwards = @java_terminal.isEchoEnabled @java_terminal.disableEcho elsif stty raw_no_echo_mode end line = "" backspace_limit = 0 begin while character = get_single_character(stty) # honor backspace and delete if character == 127 or character == 8 line.slice!(-1, 1) backspace_limit -= 1 else line << character.chr backspace_limit = line.size end # looking for carriage return (decimal 13) or # newline (decimal 10) in raw input break if character == 13 or character == 10 if @question.echo != false if character == 127 or character == 8 # only backspace if we have characters on the line to # eliminate, otherwise we'll tromp over the prompt if backspace_limit >= 0 then @output.print("\b#{HighLine.Style(:erase_char).code}") else # do nothing end else if @question.echo == true @output.print(character.chr) else @output.print(@question.echo) end end @output.flush end break if @question.limit and line.size == @question.limit end ensure if JRUBY @java_terminal.enableEcho if enable_echo_afterwards elsif stty restore_mode end end if @question.overwrite @output.print("\r#{HighLine.Style(:erase_line).code}") @output.flush else say("\n") end @question.change_case(@question.remove_whitespace(line)) end else if JRUBY enable_echo_afterwards = @java_terminal.isEchoEnabled @java_terminal.disableEcho end begin if @question.character == :getc response = get_single_character(true).chr else response = get_single_character(stty).chr if @question.overwrite @output.print("\r#{HighLine.Style(:erase_line).code}") @output.flush else echo = if @question.echo == true response elsif @question.echo != false @question.echo else "" end say("#{echo}\n") end end ensure if JRUBY @java_terminal.enableEcho if enable_echo_afterwards end end @question.change_case(response) end end # # Page print a series of at most _page_at_ lines for _output_. After each # page is printed, HighLine will pause until the user presses enter/return # then display the next page of data. # # Note that the final page of _output_ is *not* printed, but returned # instead. This is to support any special handling for the final sequence. # def page_print( output ) lines = output.scan(/[^\n]*\n?/) while lines.size > @page_at @output.puts lines.slice!(0...@page_at).join @output.puts # Return last line if user wants to abort paging return (["...\n"] + lines.slice(-2,1)).join unless continue_paging? end return lines.join end # # Ask user if they wish to continue paging output. Allows them to type "q" to # cancel the paging process. # def continue_paging? command = HighLine.new(@input, @output).ask( "-- press enter/return to continue or q to stop -- " ) { |q| q.character = true } command !~ /\A[qQ]\Z/ # Only continue paging if Q was not hit. end # # Wrap a sequence of _lines_ at _wrap_at_ characters per line. Existing # newlines will not be affected by this process, but additional newlines # may be added. # def wrap( text ) wrapped = [ ] text.each_line do |line| while line =~ /([^\n]{#{@wrap_at + 1},})/ search = $1.dup replace = $1.dup if index = replace.rindex(" ", @wrap_at) replace[index, 1] = "\n" replace.sub!(/\n[ \t]+/, "\n") line.sub!(search, replace) else line[$~.begin(1) + @wrap_at, 0] = "\n" end end wrapped << line end return wrapped.join end # # Returns the length of the passed +string_with_escapes+, minus and color # sequence escapes. # def actual_length( string_with_escapes ) string_with_escapes.to_s.gsub(/\e\[\d{1,2}m/, "").length end end require "highline/string_extensions"