class Canis::Bottomline
some variables are polluting space of including app, we should make this a class.
Attributes
Public Class Methods
# File lib/canis/core/util/extras/bottomline.rb, line 94 def initialize win=nil, row=nil @window = win @message_row = 0 # 2011-10-8 end
Public Instance Methods
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/canis/core/util/extras/bottomline.rb, line 1635 def XXXchoose( *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
# File lib/canis/core/util/extras/bottomline.rb, line 1503 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 q.limit = 1 if character yield q if block_given? end end
— highline classes }}}
# File lib/canis/core/util/extras/bottomline.rb, line 977 def ask(question, answer_type=String, &details) $log.debug "XXXX inside ask win #{@window} " @window ||= _create_footer_window #@window.show #unless @window.visible? @question ||= Question.new(question, answer_type, &details) say(@question) #unless @question.echo == true @completion_proc = @question.completion_proc @change_proc = @question.change_proc @key_handler_proc = @question.key_handler_proc @default = @question.default $log.debug "XXX: ASK RBGETS got default: #{@default} " @help_text = @question.help_text @answer_type = @question.answer_type if @question.answer_type.is_a? Array @completion_proc = Proc.new{|str| @answer_type.dup.grep Regexp.new("^#{str}") } end begin # FIXME a C-c still returns default to user ! @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 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 rescue Interrupt $log.warn "User interrupted ask() get_response does not want operation to proceed" return nil ensure @question = nil # Reset Question object. $log.debug "XXX: HIDE B AT ENSURE OF ASK" hide_bottomline # assuming this method made it visible, not sure if this is called. end end
Allows a selection in which options are shown over prompt. As user types options are narrowed down. NOTE: For a directory we are not showing a slash, so currently you have to enter the slash manually when searching. FIXME we can put remarks in fron as in memacs such as [No matches] or [single completion] @param [Array] a list of items to select from NOTE: if you use this please copy it to your app. This does not conform to highline's choose, and I'd like to somehow get it to be identical.
# File lib/canis/core/util/extras/bottomline.rb, line 1524 def choose list1, config={} dirlist = true start = 0 case list1 when NilClass #list1 = Dir.glob("*") list1 = Dir.glob("*").collect { |f| File.directory?(f) ? f+"/" : f } when String list1 = Dir.glob(list1).collect { |f| File.directory?(f) ? f+"/" : f } when Array dirlist = false # let it be, that's how it should come else # Dir listing as default #list1 = Dir.glob("*") list1 = Dir.glob("*").collect { |f| File.directory?(f) ? f+"/" : f } end require 'canis/core/util/rcommandwindow' prompt = config[:prompt] || "Choose: " layout = { :height => 5, :width => Ncurses.COLS-1, :top => Ncurses.LINES-6, :left => 0 } rc = CommandWindow.new nil, :layout => layout, :box => true, :title => config[:title] begin w = rc.window rc.display_menu list1 # earlier wmove bombed, now move is (window.rb 121) str = ask(prompt) { |q| q.help_text = config[:help_text] ; q.change_proc = Proc.new { |str| w.wmove(1,1) ; w.wclrtobot; l = list1.select{|e| e.index(str)==0} ; # select those starting with str if (l.size == 0 || str[-1]=='/') && dirlist # used to help complete directories so we can drill up and down #l = Dir.glob(str+"*") l = Dir.glob(str +"*").collect { |f| File.directory?(f) ? f+"/" : f } end rc.display_menu l; l } q.key_handler_proc = Proc.new { |ch| # this is not very good since it does not respect above list which is filtered # # need to clear the screen before printing - FIXME case ch when ?\C-n.getbyte(0) start += 2 if start < list1.length - 2 w.wmove(1,1) ; w.wclrtobot; rc.display_menu list1, :startindex => start when ?\C-p.getbyte(0) start -= 2 if start > 2 w.wmove(1,1) ; w.wclrtobot; rc.display_menu list1, :startindex => start else alert "unhalderlind by jey " end } } # need some validation here that its in the list TODO ensure rc.destroy rc = nil $log.debug "XXX: HIDE B IN ENSURE" hide_bottomline # since we called ask() we need to close bottomline end $log.debug "XXX: HIDE B AT END OF ASK" #hide_bottomline # since we called ask() we need to close bottomline return str end
clears line from 0, not okay in some cases
# File lib/canis/core/util/extras/bottomline.rb, line 1477 def clear_line len=100, from=0 print_str("%-*s" % [len," "], :y => from) end
destroy window, to be called by app when shutting down since we are normally hiding the window only.
# File lib/canis/core/util/extras/bottomline.rb, line 1083 def destroy $log.debug "bottomline destroy... #{@window} " @window.destroy if @window @window = nil end
XXX FIXME this uses only rcommand so what is it doing here.
def display_list_interactive text, config={}
returns a ListObject since you may not know what the list itself contained You can do ret.list[ret.current_index] to get value
# File lib/canis/core/util/extras/bottomline.rb, line 1610 def display_list text, config={} require 'canis/core/util/rcommandwindow' ht = config[:height] || 15 layout = { :height => ht, :width => Ncurses.COLS-1, :top => Ncurses.LINES-ht+1, :left => 0 } rc = CommandWindow.new nil, :layout => layout, :box => true, :title => config[:title] w = rc.window ret = rc.display_interactive text rc = nil ret end
XXX FIXME this uses only rcommand so what is it doing here.
# File lib/canis/core/util/extras/bottomline.rb, line 1592 def display_text_interactive text, config={} require 'canis/core/util/rcommandwindow' ht = config[:height] || 15 layout = { :height => ht, :width => Ncurses.COLS-1, :top => Ncurses.LINES-ht+1, :left => 0 } rc = CommandWindow.new nil, :layout => layout, :box => true, :title => config[:title] w = rc.window #rc.text "There was a quick brown fox who ran over the lazy dog and then went over the moon over and over again and again" rc.display_interactive(text) { |l| l.focussed_attrib = 'bold' # Ncurses::A_UNDERLINE l.focussed_symbol = '>' } rc = nil end
A helper method for sending the output stream and error and repeat of the question.
FIXME: since we write on one line in say, this often gets overidden by next say or ask
# File lib/canis/core/util/extras/bottomline.rb, line 1160 def explain_error( error ) say_with_pause(@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
# File lib/canis/core/util/extras/bottomline.rb, line 1487 def get_response return @question.first_answer if @question.first_answer? # we always use character reader, so user's value does not matter #if @question.character.nil? # if @question.echo == true #and @question.limit.nil? $log.debug "XXX: before RBGETS got default: #{@default} " ret, str = rb_getstr if ret == 0 return @question.change_case(@question.remove_whitespace(str)) end if ret == -1 raise Interrupt end return "" end
bottomline user has to hide window if he called say().
Call this if you find the window persists after using some method from here usually say or ask. NOTE: after callign this you must call window.show. Otherwise, next time you call this, it will not hide.
@param [int, float] time to sleep before hiding window.
# File lib/canis/core/util/extras/bottomline.rb, line 1066 def hide wait=nil if @window $log.debug "XXX: HIDE BOTTOMLINE INSIDE" sleep(wait) if wait #if @window.visible? #@window.hide # THIS HAS SUDDENLY STOPPED WORKING @window.destroy @window = nil #@window.wrefresh #Ncurses::Panel.update_panels #end end end
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/canis/core/util/extras/bottomline.rb, line 1675 def list( items, mode = :rows, option = nil ) items = items.to_ary.map do |item| ERB.new(item, nil, "%").result(binding) end case mode when :inline option = " or " if option.nil? case items.size when 0 "" when 1 items.first when 2 "#{items.first}#{option}#{items.last}" 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.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 else items.map { |i| "#{i}\n" }.join end end
# File lib/canis/core/util/extras/bottomline.rb, line 1481 def print_help(help_text) # best to popup a window and hsow that with ENTER to dispell print_str("%-*s" % [help_text.length+2," "]) print_str("%s" % help_text) sleep(5) end
Internal method for printing a string
# File lib/canis/core/util/extras/bottomline.rb, line 1172 def print_str(text, config={}) win = config.fetch(:window, @window) # assuming its in App x = config.fetch :x, 0 # @message_row # Ncurses.LINES-1, 0 since one line window 2011-10-8 y = config.fetch :y, 0 $log.debug "XXX: print_str #{win} with text : #{text} at #{x} #{y} " color = config[:color_pair] || $datacolor raise "no window for ask print in #{self.class} name: #{name} " unless win color=Ncurses.COLOR_PAIR(color); win.attron(color); #win.mvprintw(x, y, "%-40s" % text); win.mvprintw(x, y, "%s" % text); win.attroff(color); win.refresh # FFI NW 2011-09-9 , added back gets overwritten end
actual input routine, gets each character from user, taking care of echo, limit, completion proc, and some control characters such as C-a, C-e, C-k Taken from io.rb, has some improvements to it. However, does not print the prompt any longer Completion proc is vim style, on pressing tab it cycles through options
# File lib/canis/core/util/extras/bottomline.rb, line 1192 def rb_getstr r = @message_row c = 0 win = @window @limit = @question.limit @history = @question.history @history_list = History.new(@history) maxlen = @limit || 100 # fixme raise "rb_getstr got no window. bottomline.rb" if win.nil? ins_mode = false oldstr = nil # for tab completion, origal word entered by user default = @default || "" $log.debug "XXX: RBGETS got default: #{@default} " if @default && @history if !@history.include?(default) @history_list.push default end end len = @prompt_length # clear the area of len+maxlen color = $datacolor str = "" #str = default cpentries = nil #clear_line len+maxlen+1 #print_str(prompt+str) print_str(str, :y => @prompt_length+0) if @default len = @prompt_length + str.length begin Ncurses.noecho(); curpos = str.length prevchar = 0 entries = nil while true ch=win.getchar() $log.debug " XXXX FFI rb_getstr got ch:#{ch}, str:#{str}. " case ch when 3 # -1 # C-c # sometimes this causes an interrupt and crash return -1, nil when ?\C-g.getbyte(0) # ABORT, emacs style return -1, nil when 10, 13 # hits ENTER, complete entry and return @history_list.push str break when ?\C-h.getbyte(0), ?\C-?.getbyte(0), KEY_BSPACE, 263 # delete previous character/backspace # C-h is giving 263 i/o 8. 2011-09-19 len -= 1 if len > @prompt_length curpos -= 1 if curpos > 0 str.slice!(curpos) clear_line len+maxlen+1, @prompt_length when 330 # delete character on cursor str.slice!(curpos) #rescue next clear_line len+maxlen+1, @prompt_length when ?\M-h.getbyte(0) # HELP KEY help_text = @help_text || "No help provided..." print_help(help_text) clear_line len+maxlen+1 print_str @statement # UGH #return 7, nil #next when KEY_LEFT curpos -= 1 if curpos > 0 len -= 1 if len > @prompt_length win.move r, c+len # since getchar is not going back on del and bs wmove to move FFIWINDOW win.wrefresh next when KEY_RIGHT if curpos < str.length curpos += 1 #if curpos < str.length len += 1 win.move r, c+len # since getchar is not going back on del and bs win.wrefresh end next when ?\C-a.getbyte(0) #olen = str.length clear_line len+maxlen+1, @prompt_length len -= curpos curpos = 0 win.move r, c+len # since getchar is not going back on del and bs when ?\C-e.getbyte(0) olen = str.length len += (olen - curpos) curpos = olen clear_line len+maxlen+1, @prompt_length win.move r, c+len # since getchar is not going back on del and bs when ?\M-i.getbyte(0) ins_mode = !ins_mode next when ?\C-k.getbyte(0) # delete forward @delete_buffer = str.slice!(curpos..-1) #rescue next clear_line len+maxlen+1, @prompt_length when ?\C-u.getbyte(0) # delete to the left of cursor till start of line @delete_buffer = str.slice!(0..curpos-1) #rescue next curpos = 0 clear_line len+maxlen+1, @prompt_length len = @prompt_length when ?\C-y.getbyte(0) # paste what's in delete buffer if @delete_buffer olen = str.length str << @delete_buffer if @delete_buffer curpos = str.length len += str.length - olen end when KEY_TAB # TAB if !@completion_proc.nil? # place cursor at end of completion # after all completions, what user entered should come back so he can edit it if prevchar == 9 if !entries.nil? and !entries.empty? olen = str.length str = entries.delete_at(0) str = str.to_s.dup #str = entries[@current_index].dup #@current_index += 1 #@current_index = 0 if @current_index == entries.length curpos = str.length len += str.length - olen clear_line len+maxlen+1, @prompt_length else olen = str.length str = oldstr if oldstr curpos = str.length len += str.length - olen clear_line len+maxlen+1, @prompt_length prevchar = ch = nil # so it can start again completing end else #@current_index = 0 tabc = @completion_proc unless tabc next unless tabc oldstr = str.dup olen = str.length entries = tabc.call(str).dup $log.debug "XXX tab [#{str}] got #{entries} " str = entries.delete_at(0) unless entries.nil? or entries.empty? #str = entries[@current_index].dup unless entries.nil? or entries.empty? #@current_index += 1 #@current_index = 0 if @current_index == entries.length str = str.to_s.dup if str curpos = str.length len += str.length - olen else alert "NO MORE 2" end end else # there's another type of completion that bash does, which is irritating # compared to what vim does, it does partial completion if cpentries olen = str.length if cpentries.size == 1 str = cpentries.first.dup elsif cpentries.size > 1 str = shortest_match(cpentries).dup end curpos = str.length len += str.length - olen end end when ?\C-a.getbyte(0) .. ?\C-z.getbyte(0) # here' swhere i wish i could pass stuff back without closing # I'd like the user to be able to scroll list or do something based on # control or other keys if @key_handler_proc # added 2011-11-3 7:38 PM @key_handler_proc.call(ch) next else Ncurses.beep end when KEY_UP if @history && !@history.empty? olen = str.length str = if prevchar == KEY_UP @history_list.previous elsif prevchar == KEY_DOWN @history_list.previous else @history_list.last end str = str.dup curpos = str.length len += str.length - olen clear_line len+maxlen+1, @prompt_length else # try to pick up default, seems we don't get it 2011-10-14 if @default olen = str.length str = @default str = str.dup curpos = str.length len += str.length - olen clear_line len+maxlen+1, @prompt_length end end when KEY_DOWN if @history && !@history.empty? olen = str.length str = if prevchar == KEY_UP @history_list.next elsif prevchar == KEY_DOWN @history_list.next else @history_list.first end str = str.dup curpos = str.length len += str.length - olen clear_line len+maxlen+1, @prompt_length end else if ch < 0 || ch > 255 Ncurses.beep next end # if control char, beep if ch.chr =~ /[[:cntrl:]]/ Ncurses.beep next end # we need to trap KEY_LEFT and RIGHT and what of UP for history ? if ins_mode str[curpos] = ch.chr else str.insert(curpos, ch.chr) # FIXME index out of range due to changeproc end len += 1 curpos += 1 break if str.length >= maxlen end case @question.echo when true begin cpentries = @change_proc.call(str) if @change_proc # added 2010-11-09 23:28 rescue => exc $log.error "bottomline: change_proc EXC #{exc} " if $log.debug? $log.error( exc) if exc $log.error(exc.backtrace.join("\n")) if exc Ncurses.error end print_str(str, :y => @prompt_length+0) when false # noop, no echoing what is typed else print_str(@question.echo * str.length, :y => @prompt_length+0) end win.move r, c+len # more for arrow keys, curpos may not be end win.wrefresh # 2011-10-10 prevchar = ch end $log.debug "XXXW bottomline: after while loop" str = default if str == "" ensure Ncurses.noecho(); end return 0, str end
The basic output method for HighLine objects.
The statement parameter is processed as an ERb template, supporting embedded Ruby code. The template is evaluated with a binding inside the HighLine instance. NOTE: modified from original highline, does not care about space at end of question. Also, ansi color constants will not work. Be careful what ruby code you pass in.
NOTE: This uses a window, so it will persist in the last row. You must call hide_bottomline
to remove the window. It is preferable to call say_with_pause
from user programs
# File lib/canis/core/util/extras/bottomline.rb, line 1103 def say statement, config={} @window ||= _create_footer_window #@window.show #unless @window.visible? $log.debug "XXX: inside say win #{@window} !" case statement when Question if config.has_key? :color_pair $log.debug "INSIDE QUESTION 2 " if $log.debug? else $log.debug "XXXX SAY using colorpair: #{statement.color_pair} " if $log.debug? config[:color_pair] = statement.color_pair end else $log.debug "XXX INSDIE SAY #{statement.class} " if $log.debug? end statement = statement.to_str template = ERB.new(statement, nil, "%") statement = template.result(binding) @prompt_length = statement.length # required by ask since it prints after @statement = statement # clear_line print_str statement, config end
display some text at bottom and wait for a key before hiding window
# File lib/canis/core/util/extras/bottomline.rb, line 1131 def say_with_pause statement, config={} @window ||= _create_footer_window #@window.show #unless @window.visible? # 2011-10-14 23:52:52 say statement, config @window.wrefresh Ncurses::Panel.update_panels ch=@window.getchar() hide_bottomline ## return char so we can use for asking for one character return ch end
since say does not leave the screen, it is not exactly recommended
as it will hide what's below. It's better to call pause, or this, which will quickly go off. If the message is not important enough to ask for a pause, the will flicker on screen, but not for too long.
# File lib/canis/core/util/extras/bottomline.rb, line 1146 def say_with_wait statement, config={} @window ||= _create_footer_window #@window.show #unless @window.visible? # 2011-10-14 23:52:59 say statement, config @window.wrefresh Ncurses::Panel.update_panels sleep 0.5 hide_bottomline end
compares entries in array and returns longest common starting string as happens in bash when pressing tab abc abd abe will return ab
# File lib/canis/core/util/extras/bottomline.rb, line 1460 def shortest_match a #return "" if a.nil? || a.empty? # should not be called in such situations raise "shortest_match should not be called with nil or empty array" if a.nil? || a.empty? # should not be called in such situations as caller program will err. l = a.inject do |memo,word| str = "" 0.upto(memo.size) do |i| if memo[0..i] == word[0..i] str = memo[0..i] else break end end str end end