require "date" require "erb" require 'pathname' =begin * Name : bottomline.rb * Description : routines for input at bottom of screen like vim, or anyother line * : * Author : rkumar * Date : 2010-10-25 12:45 * License : Same as Ruby's License (http://www.ruby-lang.org/LICENSE.txt) The character input routines are from io.rb, however, the user-interface to the input is copied from the Highline project (James Earl Gray) with permission. May later use a Label and Field. NOTE : Pls avoid directly using this class. I am trying to redo this so ask, agree and say can create their own window and be done with it. The hurdle in that is that ask calls say, so when to close the window is not clear within say. Some shakeup is expected by 1.4.0 or so. =end module RubyCurses # just so the program does not bomb due to a tiny feature # I do not raise error on nil array, i create a dummy array # which you likely will not be able to use, in any case it will have only one value class History < Struct.new(:array, :current_index) attr_reader :last_index attr_reader :current_index attr_reader :array def initialize a=nil, c=0 #raise "Array passed to History cannot be nil" unless a #@max_index = a.size @array = a || [] @current_index = c @last_index = c end def last @current_index = max_index @array.last end def first @current_index = 0 @array.first end def max_index @array.size - 1 end def up item = @array[@current_index] previous return item end def next @last_index = @current_index if @current_index + 1 > max_index @current_index = 0 else @current_index += 1 end @array[@current_index] end def previous @last_index = @current_index if @current_index - 1 < 0 @current_index = max_index() else @current_index -= 1 end @array[@current_index] end def is_last? @current_index == max_index() end def push item $log.debug " XXX history push #{item} " if $log.debug? @array.push item @current_index = max_index end end # class # some variables are polluting space of including app, # we should make this a class. class Bottomline attr_accessor :window attr_accessor :message_row attr_accessor :name # for debugging def initialize win=nil, row=nil @window = win #@window.wrefresh #Ncurses::Panel.update_panels #@message_row = row @message_row = 0 # 2011-10-8 end # # create a window at bottom and show and hide it. # Causing a stack overflow since Window creates a bottomline too ! # def _create_footer_window h = 1 , w = Ncurses.COLS, t = Ncurses.LINES-1, l = 0 ewin = VER::Window.new(h, w , t, l) #ewin.bkgd(Ncurses.COLOR_PAIR($promptcolor)); @window = ewin return ewin end class QuestionError < StandardError # do nothing, just creating a unique error type end class Question # An internal HighLine error. User code does not need to trap this. class NoAutoCompleteMatch < StandardError # do nothing, just creating a unique error type end # # Create an instance of HighLine::Question. Expects a _question_ to ask # (can be "") and an _answer_type_ to convert the answer to. # The _answer_type_ parameter must be a type recognized by # Question.convert(). If given, a block is yeilded the new Question # object to allow custom initializaion. # def initialize( question, answer_type ) # initialize instance data @question = question @answer_type = answer_type @character = nil @limit = nil @echo = true @readline = false @whitespace = :strip @_case = nil @default = nil @validate = nil @above = nil @below = nil @in = nil @confirm = nil @gather = false @first_answer = nil @directory = Pathname.new(File.expand_path(File.dirname($0))) @glob = "*" @responses = Hash.new @overwrite = false @history = nil # allow block to override settings yield self if block_given? #$log.debug " XXX default #{@default}" if $log.debug? #$log.debug " XXX history #{@history}" if $log.debug? # finalize responses based on settings build_responses end # The ERb template of the question to be asked. attr_accessor :question # The type that will be used to convert this answer. attr_accessor :answer_type # # Can be set to +true+ to use HighLine's cross-platform character reader # instead of fetching an entire line of input. (Note: HighLine's character # reader *ONLY* supports STDIN on Windows and Unix.) Can also be set to # :getc to use that method on the input stream. # # *WARNING*: The _echo_ and _overwrite_ attributes for a question are # ignored when using the :getc method. # attr_accessor :character # # Allows you to set a character limit for input. # # If not set, a default of 100 is used # attr_accessor :limit # # Can be set to +true+ or +false+ to control whether or not input will # be echoed back to the user. A setting of +true+ will cause echo to # match input, but any other true value will be treated as to String to # echo for each character typed. # # This requires HighLine's character reader. See the _character_ # attribute for details. # # *Note*: When using HighLine to manage echo on Unix based systems, we # recommend installing the termios gem. Without it, it's possible to type # fast enough to have letters still show up (when reading character by # character only). # attr_accessor :echo # # Use the Readline library to fetch input. This allows input editing as # well as keeping a history. In addition, tab will auto-complete # within an Array of choices or a file listing. # # *WARNING*: This option is incompatible with all of HighLine's # character reading modes and it causes HighLine to ignore the # specified _input_ stream. # # this messes up in ncurses RK 2010-10-24 12:23 attr_accessor :readline # # Used to control whitespace processing for the answer to this question. # See HighLine::Question.remove_whitespace() for acceptable settings. # attr_accessor :whitespace # # Used to control character case processing for the answer to this question. # See HighLine::Question.change_case() for acceptable settings. # attr_accessor :_case # Used to provide a default answer to this question. attr_accessor :default # # If set to a Regexp, the answer must match (before type conversion). # Can also be set to a Proc which will be called with the provided # answer to validate with a +true+ or +false+ return. # attr_accessor :validate # Used to control range checks for answer. attr_accessor :above, :below # If set, answer must pass an include?() check on this object. attr_accessor :in # # Asks a yes or no confirmation question, to ensure a user knows what # they have just agreed to. If set to +true+ the question will be, # "Are you sure? " Any other true value for this attribute is assumed # to be the question to ask. When +false+ or +nil+ (the default), # answers are not confirmed. # attr_accessor :confirm # # When set, the user will be prompted for multiple answers which will # be collected into an Array or Hash and returned as the final answer. # # You can set _gather_ to an Integer to have an Array of exactly that # many answers collected, or a String/Regexp to match an end input which # will not be returned in the Array. # # Optionally _gather_ can be set to a Hash. In this case, the question # will be asked once for each key and the answers will be returned in a # Hash, mapped by key. The @key variable is set before each # question is evaluated, so you can use it in your question. # attr_accessor :gather # # When set to a non *nil* value, this will be tried as an answer to the # question. If this answer passes validations, it will become the result # without the user ever being prompted. Otherwise this value is discarded, # and this Question is resolved as a normal call to HighLine.ask(). # attr_writer :first_answer # # The directory from which a user will be allowed to select files, when # File or Pathname is specified as an _answer_type_. Initially set to # Pathname.new(File.expand_path(File.dirname($0))). # attr_accessor :directory # # The glob pattern used to limit file selection when File or Pathname is # specified as an _answer_type_. Initially set to "*". # attr_accessor :glob # # A Hash that stores the various responses used by HighLine to notify # the user. The currently used responses and their purpose are as # follows: # # :ambiguous_completion:: Used to notify the user of an # ambiguous answer the auto-completion # system cannot resolve. # :ask_on_error:: This is the question that will be # redisplayed to the user in the event # of an error. Can be set to # :question to repeat the # original question. # :invalid_type:: The error message shown when a type # conversion fails. # :no_completion:: Used to notify the user that their # selection does not have a valid # auto-completion match. # :not_in_range:: Used to notify the user that a # provided answer did not satisfy # the range requirement tests. # :not_valid:: The error message shown when # validation checks fail. # attr_reader :responses # # When set to +true+ the question is asked, but output does not progress to # the next line. The Cursor is moved back to the beginning of the question # line and it is cleared so that all the contents of the line disappear from # the screen. # attr_accessor :overwrite # # If the user presses tab in ask(), then this proc is used to fill in # values. Typically, for files. e.g. # # q.completion_proc = Proc.new {|str| Dir.glob(str +"*") } # attr_accessor :completion_proc # # Called when any character is pressed with the string. # # q.change_proc = Proc.new {|str| Dir.glob(str +"*") } # attr_accessor :change_proc # # Called when any control-key is pressed, one that we are not handling # # q.key_handler_proc = Proc.new {|ch| xxxx) } # attr_accessor :key_handler_proc # # text to be shown if user presses M-h # attr_accessor :helptext attr_accessor :color_pair attr_accessor :history # # Returns the provided _answer_string_ or the default answer for this # Question if a default was set and the answer is empty. # NOTE: in our case, the user actually edits this value (in highline it # is used if user enters blank) # def answer_or_default( answer_string ) if answer_string.length == 0 and not @default.nil? @default else answer_string end end # # Called late in the initialization process to build intelligent # responses based on the details of this Question object. # def build_responses( ) ### WARNING: This code is quasi-duplicated in ### ### Menu.update_responses(). Check there too when ### ### making changes! ### append_default unless default.nil? @responses = { :ambiguous_completion => "Ambiguous choice. " + "Please choose one of #{@answer_type.inspect}.", :ask_on_error => "? ", :invalid_type => "You must enter a valid #{@answer_type}.", :no_completion => "You must choose one of " + "#{@answer_type.inspect}.", :not_in_range => "Your answer isn't within the expected range " + "(#{expected_range}).", :not_valid => "Your answer isn't valid (must match " + "#{@validate.inspect})." }.merge(@responses) ### WARNING: This code is quasi-duplicated in ### ### Menu.update_responses(). Check there too when ### ### making changes! ### end # # Returns the provided _answer_string_ after changing character case by # the rules of this Question. Valid settings for whitespace are: # # +nil+:: Do not alter character case. # (Default.) # :up:: Calls upcase(). # :upcase:: Calls upcase(). # :down:: Calls downcase(). # :downcase:: Calls downcase(). # :capitalize:: Calls capitalize(). # # An unrecognized choice (like :none) is treated as +nil+. # def change_case( answer_string ) if [:up, :upcase].include?(@_case) answer_string.upcase elsif [:down, :downcase].include?(@_case) answer_string.downcase elsif @_case == :capitalize answer_string.capitalize else answer_string end end # # Transforms the given _answer_string_ into the expected type for this # Question. Currently supported conversions are: # # [...]:: Answer must be a member of the passed Array. # Auto-completion is used to expand partial # answers. # lambda {...}:: Answer is passed to lambda for conversion. # Date:: Date.parse() is called with answer. # DateTime:: DateTime.parse() is called with answer. # File:: The entered file name is auto-completed in # terms of _directory_ + _glob_, opened, and # returned. # Float:: Answer is converted with Kernel.Float(). # Integer:: Answer is converted with Kernel.Integer(). # +nil+:: Answer is left in String format. (Default.) # Pathname:: Same as File, save that a Pathname object is # returned. # String:: Answer is converted with Kernel.String(). # Regexp:: Answer is fed to Regexp.new(). # Symbol:: The method to_sym() is called on answer and # the result returned. # any other Class:: The answer is passed on to # Class.parse(). # # This method throws ArgumentError, if the conversion cannot be # completed for any reason. # def convert( answer_string ) if @answer_type.nil? answer_string elsif [Float, Integer, String].include?(@answer_type) Kernel.send(@answer_type.to_s.to_sym, answer_string) elsif @answer_type == Symbol answer_string.to_sym elsif @answer_type == Regexp Regexp.new(answer_string) elsif @answer_type.is_a?(Array) or [File, Pathname].include?(@answer_type) # cheating, using OptionParser's Completion module choices = selection #choices.extend(OptionParser::Completion) #answer = choices.complete(answer_string) answer = choices # bug in completion of optparse if answer.nil? raise NoAutoCompleteMatch end if @answer_type.is_a?(Array) #answer.last # we don't need this anylonger answer_string # we have already selected elsif @answer_type == File File.open(File.join(@directory.to_s, answer_string)) else #Pathname.new(File.join(@directory.to_s, answer.last)) Pathname.new(File.join(@directory.to_s, answer_string)) end elsif [Date, DateTime].include?(@answer_type) or @answer_type.is_a?(Class) @answer_type.parse(answer_string) elsif @answer_type.is_a?(Proc) @answer_type[answer_string] end end # Returns a english explination of the current range settings. def expected_range( ) expected = [ ] expected << "above #{@above}" unless @above.nil? expected << "below #{@below}" unless @below.nil? expected << "included in #{@in.inspect}" unless @in.nil? case expected.size when 0 then "" when 1 then expected.first when 2 then expected.join(" and ") else expected[0..-2].join(", ") + ", and #{expected.last}" end end # Returns _first_answer_, which will be unset following this call. def first_answer( ) @first_answer ensure @first_answer = nil end # Returns true if _first_answer_ is set. def first_answer?( ) not @first_answer.nil? end # # Returns +true+ if the _answer_object_ is greater than the _above_ # attribute, less than the _below_ attribute and included?()ed in the # _in_ attribute. Otherwise, +false+ is returned. Any +nil+ attributes # are not checked. # def in_range?( answer_object ) (@above.nil? or answer_object > @above) and (@below.nil? or answer_object < @below) and (@in.nil? or @in.include?(answer_object)) end # # Returns the provided _answer_string_ after processing whitespace by # the rules of this Question. Valid settings for whitespace are: # # +nil+:: Do not alter whitespace. # :strip:: Calls strip(). (Default.) # :chomp:: Calls chomp(). # :collapse:: Collapses all whitspace runs to a # single space. # :strip_and_collapse:: Calls strip(), then collapses all # whitspace runs to a single space. # :chomp_and_collapse:: Calls chomp(), then collapses all # whitspace runs to a single space. # :remove:: Removes all whitespace. # # An unrecognized choice (like :none) is treated as +nil+. # # This process is skipped, for single character input. # def remove_whitespace( answer_string ) if @whitespace.nil? answer_string elsif [:strip, :chomp].include?(@whitespace) answer_string.send(@whitespace) elsif @whitespace == :collapse answer_string.gsub(/\s+/, " ") elsif [:strip_and_collapse, :chomp_and_collapse].include?(@whitespace) result = answer_string.send(@whitespace.to_s[/^[a-z]+/]) result.gsub(/\s+/, " ") elsif @whitespace == :remove answer_string.gsub(/\s+/, "") else answer_string end end # # Returns an Array of valid answers to this question. These answers are # only known when _answer_type_ is set to an Array of choices, File, or # Pathname. Any other time, this method will return an empty Array. # def selection( ) if @answer_type.is_a?(Array) @answer_type elsif [File, Pathname].include?(@answer_type) Dir[File.join(@directory.to_s, @glob)].map do |file| File.basename(file) end else [ ] end end # Stringifies the question to be asked. def to_str( ) @question end # # Returns +true+ if the provided _answer_string_ is accepted by the # _validate_ attribute or +false+ if it's not. # # It's important to realize that an answer is validated after whitespace # and case handling. # def valid_answer?( answer_string ) @validate.nil? or (@validate.is_a?(Regexp) and answer_string =~ @validate) or (@validate.is_a?(Proc) and @validate[answer_string]) end private # # Adds the default choice to the end of question between |...|. # Trailing whitespace is preserved so the function of HighLine.say() is # not affected. # def append_default( ) if @question =~ /([\t ]+)\Z/ @question << "|#{@default}|#{$1}" elsif @question == "" @question << "|#{@default}| " elsif @question[-1, 1] == "\n" @question[-2, 0] = " |#{@default}|" else @question << " |#{@default}|" end end end # class # Menu objects encapsulate all the details of a call to HighLine.choose(). # Using the accessors and Menu.choice() and Menu.choices(), the block passed # to HighLine.choose() can detail all aspects of menu display and control. # class Menu < Question # # Create an instance of HighLine::Menu. All customization is done # through the passed block, which should call accessors and choice() and # choices() as needed to define the Menu. Note that Menus are also # Questions, so all that functionality is available to the block as # well. # def initialize( ) # # Initialize Question objects with ignored values, we'll # adjust ours as needed. # super("Ignored", [ ], &nil) # avoiding passing the block along @items = [ ] @hidden_items = [ ] @help = Hash.new("There's no help for that topic.") @index = :number @index_suffix = ". " @select_by = :index_or_name @flow = :rows @list_option = nil @header = nil @prompt = "? " @layout = :list @shell = false @nil_on_handled = false # Override Questions responses, we'll set our own. @responses = { } # Context for action code. @highline = nil yield self if block_given? init_help if @shell and not @help.empty? end # # An _index_ to append to each menu item in display. See # Menu.index=() for details. # attr_reader :index # # The String placed between an _index_ and a menu item. Defaults to # ". ". Switches to " ", when _index_ is set to a String (like "-"). # attr_accessor :index_suffix # # The _select_by_ attribute controls how the user is allowed to pick a # menu item. The available choices are: # # :index:: The user is allowed to type the numerical # or alphetical index for their selection. # :index_or_name:: Allows both methods from the # :index option and the # :name option. # :name:: Menu items are selected by typing a portion # of the item name that will be # auto-completed. # attr_accessor :select_by # # This attribute is passed directly on as the mode to HighLine.list() by # all the preset layouts. See that method for appropriate settings. # attr_accessor :flow # # This setting is passed on as the third parameter to HighLine.list() # by all the preset layouts. See that method for details of its # effects. Defaults to +nil+. # attr_accessor :list_option # # Used by all the preset layouts to display title and/or introductory # information, when set. Defaults to +nil+. # attr_accessor :header # # Used by all the preset layouts to ask the actual question to fetch a # menu selection from the user. Defaults to "? ". # attr_accessor :prompt # # An ERb _layout_ to use when displaying this Menu object. See # Menu.layout=() for details. # attr_reader :layout # # When set to +true+, responses are allowed to be an entire line of # input, including details beyond the command itself. Only the first # "word" of input will be matched against the menu choices, but both the # command selected and the rest of the line will be passed to provided # action blocks. Defaults to +false+. # attr_accessor :shell # # When +true+, any selected item handled by provided action code, will # return +nil+, instead of the results to the action code. This may # prove handy when dealing with mixed menus where only the names of # items without any code (and +nil+, of course) will be returned. # Defaults to +false+. # attr_accessor :nil_on_handled # # Adds _name_ to the list of available menu items. Menu items will be # displayed in the order they are added. # # An optional _action_ can be associated with this name and if provided, # it will be called if the item is selected. The result of the method # will be returned, unless _nil_on_handled_ is set (when you would get # +nil+ instead). In _shell_ mode, a provided block will be passed the # command chosen and any details that followed the command. Otherwise, # just the command is passed. The @highline variable is set to # the current HighLine context before the action code is called and can # thus be used for adding output and the like. # def choice( name, help = nil, &action ) @items << [name, action] @help[name.to_s.downcase] = help unless help.nil? update_responses # rebuild responses based on our settings end # # A shortcut for multiple calls to the sister method choice(). Be # warned: An _action_ set here will apply to *all* provided # _names_. This is considered to be a feature, so you can easily # hand-off interface processing to a different chunk of code. # def choices( *names, &action ) names.each { |n| choice(n, &action) } end # Identical to choice(), but the item will not be listed for the user. def hidden( name, help = nil, &action ) @hidden_items << [name, action] @help[name.to_s.downcase] = help unless help.nil? end # # Sets the indexing style for this Menu object. Indexes are appended to # menu items, when displayed in list form. The available settings are: # # :number:: Menu items will be indexed numerically, starting # with 1. This is the default method of indexing. # :letter:: Items will be indexed alphabetically, starting # with a. # :none:: No index will be appended to menu items. # any String:: Will be used as the literal _index_. # # Setting the _index_ to :none a literal String, also adjusts # _index_suffix_ to a single space and _select_by_ to :none. # Because of this, you should make a habit of setting the _index_ first. # def index=( style ) @index = style # Default settings. if @index == :none or @index.is_a?(String) @index_suffix = " " @select_by = :name end end # # Initializes the help system by adding a :help choice, some # action code, and the default help listing. # def init_help( ) return if @items.include?(:help) topics = @help.keys.sort help_help = @help.include?("help") ? @help["help"] : "This command will display helpful messages about " + "functionality, like this one. To see the help for " + "a specific topic enter:\n\thelp [TOPIC]\nTry asking " + "for help on any of the following:\n\n" + "<%= list(#{topics.inspect}, :columns_across) %>" choice(:help, help_help) do |command, topic| topic.strip! topic.downcase! if topic.empty? @highline.say(@help["help"]) else @highline.say("= #{topic}\n\n#{@help[topic]}") end end end # # Used to set help for arbitrary topics. Use the topic "help" # to override the default message. # def help( topic, help ) @help[topic] = help end # # Setting a _layout_ with this method also adjusts some other attributes # of the Menu object, to ideal defaults for the chosen _layout_. To # account for that, you probably want to set a _layout_ first in your # configuration block, if needed. # # Accepted settings for _layout_ are: # # :list:: The default _layout_. The _header_ if set # will appear at the top on its own line with # a trailing colon. Then the list of menu # items will follow. Finally, the _prompt_ # will be used as the ask()-like question. # :one_line:: A shorter _layout_ that fits on one line. # The _header_ comes first followed by a # colon and spaces, then the _prompt_ with menu # items between trailing parenthesis. # :menu_only:: Just the menu items, followed up by a likely # short _prompt_. # any ERb String:: Will be taken as the literal _layout_. This # String can access @header, # @menu and @prompt, but is # otherwise evaluated in the typical HighLine # context, to provide access to utilities like # HighLine.list() primarily. # # If set to either :one_line, or :menu_only, _index_ # will default to :none and _flow_ will default to # :inline. # def layout=( new_layout ) @layout = new_layout # Default settings. case @layout when :one_line, :menu_only self.index = :none @flow = :inline end end # # This method returns all possible options for auto-completion, based # on the settings of _index_ and _select_by_. # def options( ) # add in any hidden menu commands @items.concat(@hidden_items) by_index = if @index == :letter l_index = "`" @items.map { "#{l_index.succ!}" } else (1 .. @items.size).collect { |s| String(s) } end by_name = @items.collect { |c| c.first } case @select_by when :index then by_index when :name by_name else by_index + by_name end ensure # make sure the hidden items are removed, before we return @items.slice!(@items.size - @hidden_items.size, @hidden_items.size) end # # This method processes the auto-completed user selection, based on the # rules for this Menu object. If an action was provided for the # selection, it will be executed as described in Menu.choice(). # def select( highline_context, selection, details = nil ) # add in any hidden menu commands @items.concat(@hidden_items) # Find the selected action. name, action = if selection =~ /^\d+$/ @items[selection.to_i - 1] else l_index = "`" index = @items.map { "#{l_index.succ!}" }.index(selection) $log.debug "iindex #{index}, #{@items} " if $log.debug? @items.find { |c| c.first == selection } or @items[index] end # Run or return it. if not @nil_on_handled and not action.nil? @highline = highline_context if @shell action.call(name, details) else action.call(name) end elsif action.nil? name else nil end ensure # make sure the hidden items are removed, before we return @items.slice!(@items.size - @hidden_items.size, @hidden_items.size) end # # Allows Menu objects to pass as Arrays, for use with HighLine.list(). # This method returns all menu items to be displayed, complete with # indexes. # def to_ary( ) case @index when :number @items.map { |c| "#{@items.index(c) + 1}#{@index_suffix}#{c.first}" } when :letter l_index = "`" @items.map { |c| "#{l_index.succ!}#{@index_suffix}#{c.first}" } when :none @items.map { |c| "#{c.first}" } else @items.map { |c| "#{index}#{@index_suffix}#{c.first}" } end end # # Allows Menu to behave as a String, just like Question. Returns the # _layout_ to be rendered, which is used by HighLine.say(). # def to_str( ) case @layout when :list '<%= if @header.nil? then '' else "#{@header}:\n" end %>' + "<%= list( @menu, #{@flow.inspect}, #{@list_option.inspect} ) %>" + "<%= @prompt %>" when :one_line '<%= if @header.nil? then '' else "#{@header}: " end %>' + "<%= @prompt %>" + "(<%= list( @menu, #{@flow.inspect}, #{@list_option.inspect} ) %>)" + "<%= @prompt[/\s*$/] %>" when :menu_only "<%= list( @menu, #{@flow.inspect}, #{@list_option.inspect} ) %><%= @prompt %>" else @layout end end # # This method will update the intelligent responses to account for # Menu specific differences. This overrides the work done by # Question.build_responses(). # def update_responses( ) append_default unless default.nil? @responses = @responses.merge( :ambiguous_completion => "Ambiguous choice. " + "Please choose one of #{options.inspect}.", :ask_on_error => "? ", :invalid_type => "You must enter a valid #{options}.", :no_completion => "You must choose one of " + "#{options.inspect}.", :not_in_range => "Your answer isn't within the expected range " + "(#{expected_range}).", :not_valid => "Your answer isn't valid (must match " + "#{@validate.inspect})." ) end end 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} " @helptext = @question.helptext @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 # # 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. # 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 alias :hide_bottomline :hide # # destroy window, to be called by app when shutting down # since we are normally hiding the window only. def destroy $log.debug "bottomline destroy... #{@window} " @window.destroy if @window @window = nil 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 # 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 # 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 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. 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 # 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 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 # # Internal method for printing a string # 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 def rbgetstr r = @message_row c = 0 win = @window @limit = @question.limit @history = @question.history @history_list = History.new(@history) maxlen = @limit || 100 # fixme raise "rbgetstr 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 rbgetstr 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 helptext = @helptext || "No help provided" print_help(helptext) 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 # compares entries in array and returns longest common starting string # as happens in bash when pressing tab # abc abd abe will return ab 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 # clears line from 0, not okay in some cases def clear_line len=100, from=0 print_str("%-*s" % [len," "], :y => from) end def print_help(helptext) # best to popup a window and hsow that with ENTER to dispell print_str("%-*s" % [helptext.length+2," "]) print_str("%s" % helptext) sleep(5) end 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 = rbgetstr if ret == 0 return @question.change_case(@question.remove_whitespace(str)) end if ret == -1 raise Interrupt end return "" end 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 # presents given list in numbered format in a window above last line # and accepts input on last line # The list is a list of strings. e.g. # %w{ ruby perl python haskell } # Multiple levels can be given as: # list = %w{ ruby perl python haskell } # list[0] = %w{ ruby ruby1.9 ruby 1.8 rubinius jruby } # In this case, "ruby" is the first level option. The others are used # in the second level. This might make it clearer. first3 has 2 choices under it. # [ "first1" , "first2", ["first3", "second1", "second2"], "first4"] # # Currently, we return an array containing each selected level # # @return [Array] selected option/s from list def numbered_menu list1, config={} if list1.nil? || list1.empty? say_with_pause "empty list passed to numbered_menu" return nil end prompt = config[:prompt] || "Select one: " require 'rbcurse/rcommandwindow' layout = { :height => 5, :width => Ncurses.COLS-1, :top => Ncurses.LINES-6, :left => 0 } rc = CommandWindow.new nil, :layout => layout, :box => true, :title => config[:title] w = rc.window # should we yield rc, so user can bind keys or whatever # attempt a loop so we do levels. retval = [] begin while true rc.display_menu list1, :indexing => :number ret = ask(prompt, Integer ) { |q| q.in = 1..list1.size } val = list1[ret-1] if val.is_a? Array retval << val[0] $log.debug "NL: #{retval} " list1 = val[1..-1] rc.clear else retval << val $log.debug "NL1: #{retval} " break end end ensure rc.destroy rc = nil end #list1[ret-1] $log.debug "NL2: #{retval} , #{retval.class} " retval 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. # 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 'rbcurse/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.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 def display_text_interactive text, config={} require 'rbcurse/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 #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 def display_list text, config={} require 'rbcurse/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 # # 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 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 # 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 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 end # module end # module if __FILE__ == $PROGRAM_NAME #tabc = Proc.new {|str| Dir.glob(str +"*") } require 'rbcurse/app' require 'forwardable' #include Bottomline #$tt = Bottomline.new #module Kernel #extend Forwardable #def_delegators :$tt, :ask, :say, :agree, :choose, :numbered_menu #end App.new do header = app_header "rbcurse 1.2.0", :text_center => "**** Demo", :text_right =>"New Improved!", :color => :black, :bgcolor => :white, :attr => :bold message "Press F1 to exit from here" #stack :margin_top => 2, :margin => 5, :width => 30 do #end # stack #-----------------#------------------ #choose do |menu| #menu.prompt = "Please choose your favorite programming language? " ##menu.layout = :one_line # #menu.choice :ruby do say("Good choice!") end #menu.choice(:python) do say("python Not from around here, are you?") end #menu.choice(:perl) do say("perl Not from around here, are you?") end #menu.choice(:rake) do say("rake Not from around here, are you?") end #end entry = {} entry[:file] = ask("File? ", Pathname) do |q| q.completion_proc = Proc.new {|str| Dir.glob(str +"*") } q.helptext = "Enter start of filename and tab to get completion" end alert "file: #{entry[:file]} " $log.debug "FILE: #{entry[:file]} " entry[:command] = ask("Command? ", %w{archive delete read refresh delete!}) exit unless agree("Wish to continue? ", false) entry[:address] = ask("Address? ") { |q| q.color_pair = $promptcolor } entry[:company] = ask("Company? ") { |q| q.default = "none" } entry[:password] = ask("password? ") { |q| q.echo = '*' q.limit = 4 } =begin entry[:state] = ask("State? ") do |q| q._case = :up q.validate = /\A[A-Z]{2}\Z/ q.helptext = "Enter 2 characters for your state" end entry[:zip] = ask("Zip? ") do |q| q.validate = /\A\d{5}(?:-?\d{4})?\Z/ end entry[:phone] = ask( "Phone? ", lambda { |p| p.delete("^0-9"). sub(/\A(\d{3})/, '(\1) '). sub(/(\d{4})\Z/, '-\1') } ) do |q| q.validate = lambda { |p| p.delete("^0-9").length == 10 } q.responses[:not_valid] = "Enter a phone numer with area code." end entry[:age] = ask("Age? ", Integer) { |q| q.in = 0..105 } entry[:birthday] = ask("Birthday? ", Date) entry[:interests] = ask( "Interests? (comma separated list) ", lambda { |str| str.split(/,\s*/) } ) entry[:description] = ask("Enter a description for this contact.") do |q| q.whitespace = :strip_and_collapse end =end $log.debug "ENTRY: #{entry} " if $log.debug? #puts entry end # app end # FILE