| Class | HighLine |
| In: |
lib/highline/menu.rb
lib/highline/question.rb lib/highline.rb |
| Parent: | Object |
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.
| VERSION | = | "1.2.0".freeze | The version of the installed library. | |
| CLEAR | = | "\e[0m" | Embed in a String to clear all previous ANSI sequences. This MUST be done before the program exits! | |
| RESET | = | CLEAR | An alias for CLEAR. | |
| BOLD | = | "\e[1m" | The start of an ANSI bold sequence. | |
| DARK | = | "\e[2m" | The start of an ANSI dark sequence. (Terminal support uncommon.) | |
| UNDERLINE | = | "\e[4m" | The start of an ANSI underline sequence. | |
| UNDERSCORE | = | UNDERLINE | An alias for UNDERLINE. | |
| BLINK | = | "\e[5m" | The start of an ANSI blink sequence. (Terminal support uncommon.) | |
| REVERSE | = | "\e[7m" | The start of an ANSI reverse sequence. | |
| CONCEALED | = | "\e[8m" | The start of an ANSI concealed sequence. (Terminal support uncommon.) | |
| BLACK | = | "\e[30m" | Set the terminal’s foreground ANSI color to black. | |
| RED | = | "\e[31m" | Set the terminal’s foreground ANSI color to red. | |
| GREEN | = | "\e[32m" | Set the terminal’s foreground ANSI color to green. | |
| YELLOW | = | "\e[33m" | Set the terminal’s foreground ANSI color to yellow. | |
| BLUE | = | "\e[34m" | Set the terminal’s foreground ANSI color to blue. | |
| MAGENTA | = | "\e[35m" | Set the terminal’s foreground ANSI color to magenta. | |
| CYAN | = | "\e[36m" | Set the terminal’s foreground ANSI color to cyan. | |
| WHITE | = | "\e[37m" | Set the terminal’s foreground ANSI color to white. | |
| ON_BLACK | = | "\e[40m" | Set the terminal’s background ANSI color to black. | |
| ON_RED | = | "\e[41m" | Set the terminal’s background ANSI color to red. | |
| ON_GREEN | = | "\e[42m" | Set the terminal’s background ANSI color to green. | |
| ON_YELLOW | = | "\e[43m" | Set the terminal’s background ANSI color to yellow. | |
| ON_BLUE | = | "\e[44m" | Set the terminal’s background ANSI color to blue. | |
| ON_MAGENTA | = | "\e[45m" | Set the terminal’s background ANSI color to magenta. | |
| ON_CYAN | = | "\e[46m" | Set the terminal’s background ANSI color to cyan. | |
| ON_WHITE | = | "\e[47m" | Set the terminal’s background ANSI color to white. | |
| CHARACTER_MODE | = | "Win32API" | ||
| CHARACTER_MODE | = | "termios" | ||
| CHARACTER_MODE | = | "stty" |
| page_at | [RW] | Set to an integer value to cause HighLine to page output lines over the indicated line limit. When nil, the default, no paging occurs. |
| wrap_at | [RW] | Set to an integer value to cause HighLine to wrap output lines at the indicated character limit. When nil, the default, no wrapping occurs. |
Create an instance of HighLine, connected to the streams input and output.
# File lib/highline.rb, line 110
110: def initialize( input = $stdin, output = $stdout,
111: wrap_at = nil, page_at = nil )
112: @input = input
113: @output = output
114: @wrap_at = wrap_at
115: @page_at = page_at
116:
117: @question = nil
118: @answer = nil
119: @menu = nil
120: @header = nil
121: @prompt = nil
122: @gather = nil
123: @answers = nil
124: @key = nil
125: end
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.
Raises EOFError if input is exhausted.
# File lib/highline.rb, line 146
146: def agree( yes_or_no_question, character = nil )
147: ask(yes_or_no_question, lambda { |yn| yn.downcase[0] == ?y}) do |q|
148: q.validate = /\Ay(?:es)?|no?\Z/i
149: q.responses[:not_valid] = 'Please enter "yes" or "no".'
150: q.responses[:ask_on_error] = :question
151: q.character = character
152: end
153: 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.
# File lib/highline.rb, line 169
169: def ask( question, answer_type = String, &details ) # :yields: question
170: @question ||= Question.new(question, answer_type, &details)
171:
172: return gather if @question.gather
173:
174: # readline() needs to handle it's own output
175: say(@question) unless @question.readline
176: begin
177: @answer = @question.answer_or_default(get_response)
178: unless @question.valid_answer?(@answer)
179: explain_error(:not_valid)
180: raise QuestionError
181: end
182:
183: @answer = @question.convert(@answer)
184:
185: if @question.in_range?(@answer)
186: if @question.confirm
187: # need to add a layer of scope to ask a question inside a
188: # question, without destroying instance data
189: context_change = self.class.new(@input, @output, @wrap_at, @page_at)
190: if @question.confirm == true
191: confirm_question = "Are you sure? "
192: else
193: # evaluate ERb under initial scope, so it will have
194: # access to @question and @answer
195: template = ERB.new(@question.confirm, nil, "%")
196: confirm_question = template.result(binding)
197: end
198: unless context_change.agree(confirm_question)
199: explain_error(nil)
200: raise QuestionError
201: end
202: end
203:
204: @answer
205: else
206: explain_error(:not_in_range)
207: raise QuestionError
208: end
209: rescue QuestionError
210: retry
211: rescue ArgumentError
212: explain_error(:invalid_type)
213: retry
214: rescue Question::NoAutoCompleteMatch
215: explain_error(:no_completion)
216: retry
217: rescue NameError
218: raise if $!.is_a?(NoMethodError)
219: explain_error(:ambiguous_completion)
220: retry
221: ensure
222: @question = nil # Reset Question object.
223: end
224: 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.
# File lib/highline.rb, line 241
241: def choose( *items, &details )
242: @menu = @question = Menu.new(&details)
243: @menu.choices(*items) unless items.empty?
244:
245: # Set _answer_type_ so we can double as the Question for ask().
246: @menu.answer_type = if @menu.shell
247: lambda do |command| # shell-style selection
248: first_word = command.split.first
249:
250: options = @menu.options
251: options.extend(OptionParser::Completion)
252: answer = options.complete(first_word)
253:
254: if answer.nil?
255: raise Question::NoAutoCompleteMatch
256: end
257:
258: [answer.last, command.sub(/^\s*#{first_word}\s*/, "")]
259: end
260: else
261: @menu.options # normal menu selection, by index or name
262: end
263:
264: # Provide hooks for ERb layouts.
265: @header = @menu.header
266: @prompt = @menu.prompt
267:
268: if @menu.shell
269: selected = ask("Ignored", @menu.answer_type)
270: @menu.select(self, *selected)
271: else
272: selected = ask("Ignored", @menu.answer_type)
273: @menu.select(self, selected)
274: end
275: 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.
# File lib/highline.rb, line 288
288: def color( string, *colors )
289: return string unless self.class.use_color?
290:
291: colors.map! do |c|
292: if c.is_a?(Symbol)
293: self.class.const_get(c.to_s.upcase)
294: else
295: c
296: end
297: end
298: "#{colors.join}#{string}#{CLEAR}"
299: 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. |
| :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.
# File lib/highline.rb, line 329
329: def list( items, mode = :rows, option = nil )
330: items = items.to_ary.map do |item|
331: ERB.new(item, nil, "%").result(binding)
332: end
333:
334: case mode
335: when :inline
336: option = " or " if option.nil?
337:
338: case items.size
339: when 0
340: ""
341: when 1
342: items.first
343: when 2
344: "#{items.first}#{option}#{items.last}"
345: else
346: items[0..-2].join(", ") + "#{option}#{items.last}"
347: end
348: when :columns_across, :columns_down
349: max_length = actual_length(
350: items.max { |a, b| actual_length(a) <=> actual_length(b) }
351: )
352:
353: if option.nil?
354: limit = @wrap_at || 80
355: option = (limit + 2) / (max_length + 2)
356: end
357:
358: items = items.map do |item|
359: pad = max_length + (item.length - actual_length(item))
360: "%-#{pad}s" % item
361: end
362: row_count = (items.size / option.to_f).ceil
363:
364: if mode == :columns_across
365: rows = Array.new(row_count) { Array.new }
366: items.each_with_index do |item, index|
367: rows[index / option] << item
368: end
369:
370: rows.map { |row| row.join(" ") + "\n" }.join
371: else
372: columns = Array.new(option) { Array.new }
373: items.each_with_index do |item, index|
374: columns[index / row_count] << item
375: end
376:
377: list = ""
378: columns.first.size.times do |index|
379: list << columns.map { |column| column[index] }.
380: compact.join(" ") + "\n"
381: end
382: list
383: end
384: else
385: items.map { |i| "#{i}\n" }.join
386: end
387: 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.
# File lib/highline.rb, line 399
399: def say( statement )
400: statement = statement.to_str
401: return unless statement.length > 0
402:
403: template = ERB.new(statement, nil, "%")
404: statement = template.result(binding)
405:
406: statement = wrap(statement) unless @wrap_at.nil?
407: statement = page_print(statement) unless @page_at.nil?
408:
409: if statement[-1, 1] == " " or statement[-1, 1] == "\t"
410: @output.print(statement)
411: @output.flush
412: else
413: @output.puts(statement)
414: end
415: end
# File lib/highline.rb, line 669
669: def actual_length( string_with_escapes )
670: string_with_escapes.gsub(/\e\[\d{1,2}m/, "").length
671: end
A helper method for sending the output stream and error and repeat of the question.
# File lib/highline.rb, line 423
423: def explain_error( error )
424: say(@question.responses[error]) unless error.nil?
425: if @question.responses[:ask_on_error] == :question
426: say(@question)
427: elsif @question.responses[:ask_on_error]
428: say(@question.responses[:ask_on_error])
429: end
430: end
Collects an Array/Hash full of answers as described in HighLine::Question.gather().
Raises EOFError if input is exhausted.
# File lib/highline.rb, line 438
438: def gather( )
439: @gather = @question.gather
440: @answers = [ ]
441: original_question = @question
442:
443: @question.gather = false
444:
445: case @gather
446: when Integer
447: @answers << ask(@question)
448: @gather -= 1
449:
450: original_question.question = ""
451: until @gather.zero?
452: @question = original_question
453: @answers << ask(@question)
454: @gather -= 1
455: end
456: when String, Regexp
457: @answers << ask(@question)
458:
459: original_question.question = ""
460: until (@gather.is_a?(String) and @answers.last.to_s == @gather) or
461: (@gather.is_a?(Regexp) and @answers.last.to_s =~ @gather)
462: @question = original_question
463: @answers << ask(@question)
464: end
465:
466: @answers.pop
467: when Hash
468: @answers = { }
469: @gather.keys.sort.each do |key|
470: @question = original_question
471: @key = key
472: @answers[key] = ask(@question)
473: end
474: end
475:
476: @answers
477: end
Windows savvy getc().
WARNING: This method ignores @input and reads one character from STDIN!
# File lib/highline.rb, line 494
494: def get_character
495: Win32API.new("crtdll", "_getch", [ ], "L").Call
496: end
Unix savvy getc(). (First choice.)
WARNING: This method requires the "termios" library!
# File lib/highline.rb, line 508
508: def get_character
509: old_settings = Termios.getattr(@input)
510:
511: new_settings = old_settings.dup
512: new_settings.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
513:
514: begin
515: Termios.setattr(@input, Termios::TCSANOW, new_settings)
516: @input.getc
517: ensure
518: Termios.setattr(@input, Termios::TCSANOW, old_settings)
519: end
520: end
Unix savvy getc(). (Second choice.)
WARNING: This method requires the external "stty" program!
# File lib/highline.rb, line 529
529: def get_character
530: state = `stty -g`
531:
532: begin
533: system "stty raw -echo cbreak"
534: @input.getc
535: ensure
536: system "stty #{state}"
537: end
538: 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.
# File lib/highline.rb, line 551
551: def get_line( )
552: if @question.readline
553: require "readline" # load only if needed
554:
555: # capture say()'s work in a String to feed to readline()
556: old_output = @output
557: @output = StringIO.new
558: say(@question)
559: question = @output.string
560: @output = old_output
561:
562: # prep auto-completion
563: completions = @question.selection.abbrev
564: Readline.completion_proc = lambda { |string| completions[string] }
565:
566: # work-around ugly readline() warnings
567: old_verbose = $VERBOSE
568: $VERBOSE = nil
569: answer = @question.change_case(
570: @question.remove_whitespace(
571: Readline.readline(question, true) ) )
572: $VERBOSE = old_verbose
573:
574: answer
575: else
576: raise EOFError, "The input stream is exhausted." if @input.eof?
577:
578: @question.change_case(@question.remove_whitespace(@input.gets))
579: end
580: 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.
# File lib/highline.rb, line 591
591: def get_response( )
592: return @question.first_answer if @question.first_answer?
593:
594: if @question.character.nil?
595: if @question.echo == true and @question.limit.nil?
596: get_line
597: else
598: line = ""
599: while character = get_character
600: line << character.chr
601: # looking for carriage return (decimal 13) or
602: # newline (decimal 10) in raw input
603: break if character == 13 or character == 10 or
604: (@question.limit and line.size == @question.limit)
605: @output.print(@question.echo) if @question.echo != false
606: end
607: say("\n")
608: @question.change_case(@question.remove_whitespace(line))
609: end
610: elsif @question.character == :getc
611: @question.change_case(@input.getc.chr)
612: else
613: response = get_character.chr
614: echo = if @question.echo == true
615: response
616: elsif @question.echo != false
617: @question.echo
618: else
619: ""
620: end
621: say("#{echo}\n")
622: @question.change_case(response)
623: end
624: 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.
# File lib/highline.rb, line 634
634: def page_print( output )
635: lines = output.scan(/[^\n]*\n?/)
636: while lines.size > @page_at
637: @output.puts lines.slice!(0...@page_at).join
638: @output.puts
639: ask("-- press enter/return to continue -- ")
640: @output.puts
641: end
642: return lines.join
643: 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.
# File lib/highline.rb, line 650
650: def wrap( lines )
651: wrapped = [ ]
652: lines.each do |line|
653: while line =~ /([^\n]{#{@wrap_at + 1},})/
654: search = $1.dup
655: replace = $1.dup
656: if index = replace.rindex(" ", @wrap_at)
657: replace[index, 1] = "\n"
658: replace.sub!(/\n[ \t]+/, "\n")
659: line.sub!(search, replace)
660: else
661: line[@wrap_at, 0] = "\n"
662: end
663: end
664: wrapped << line
665: end
666: return wrapped.join
667: end