# coding: utf-8 #-- # menu.rb # # Created by Gregory Thomas Brown on 2005-05-10. # Copyright 2005. All rights reserved. # # This is Free Software. See LICENSE and COPYING for details. require "highline/question" require "highline/menu/item" class HighLine # # Menu objects encapsulate all the details of a call to {HighLine#choose 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 # Pass +false+ to _color_ to turn off HighLine::Menu's # index coloring. # Pass a color and the Menu's indices will be colored. class << self attr_writer :index_color end # Initialize it self.index_color = false # Returns color used for coloring Menu's indices class << self attr_reader :index_color end # # Create an instance of HighLine::Menu. All customization is done # through the passed block, which should call accessors, {#choice} and # {#choices} as needed to define the Menu. Note that Menus are also # {HighLine::Question Questions}, so all that functionality is available # to the block as well. # # @example Implicit menu creation through HighLine#choose # cli = HighLine.new # answer = cli.choose do |menu| # menu.prompt = "Please choose your favorite programming language? " # menu.choice(:ruby) { say("Good choice!") } # menu.choices(:python, :perl) { say("Not from around here, are you?") } # end 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 # Used for coloring Menu indices. # Set it to default. But you may override it. @index_color = self.class.index_color # Override Questions responses, we'll set our own. @responses = {} # Context for action code. @highline = nil yield self if block_given? init_help if @shell && !@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 alphabetical 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 # # The color of the index when displaying the menu. See Style class for # available colors. # attr_accessor :index_color # # 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. # # @param name [#to_s] menu item title/header/name to be displayed. # @param action [Proc] callback action to be run when the item is selected. # @param help [String] help/hint string to be displayed. # @return [void] # @example (see HighLine::Menu#initialize) # @example Use of help string on menu items # cli = HighLine.new # cli.choose do |menu| # menu.shell = true # # menu.choice(:load, text: 'Load a file', help: "Load a file using your favourite editor.") # menu.choice(:save, help: "Save data in file.") # menu.choice(:quit, help: "Exit program.") # # menu.help("rules", "The rules of this system are as follows...") # end def choice(name, help = nil, text = nil, &action) item = Menu::Item.new(name, text: text, help: help, action: action) @items << item @help.merge!(item.item_help) update_responses # rebuild responses based on our settings end # # This method helps reduce the namespaces in the original call, which would look # like this: HighLine::Menu::Item.new(...) # With #build_item, it looks like this: menu.build_item(...) # @param *args splat args, the same args you would pass to an initialization of # HighLine::Menu::Item # @return [HighLine::Menu::Item] the menu item def build_item(*args) Menu::Item.new(*args) end # # Adds an item directly to the menu. If you want more configuraiton or options, # use this method # # @param item [Menu::Item] item containing choice fields and more # @return [void] def add_item(item) @items << item @help.merge!(item.item_help) update_responses 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. # @param names [Array<#to_s>] menu item titles/headers/names to be displayed. # @param action (see #choice) # @return [void] # @example (see HighLine::Menu#initialize) # # choice has more options available to you, like longer text or help (and # of course, individual actions) # def choices(*names, &action) names.each { |n| choice(n, &action) } end # Identical to {#choice}, but the item will not be listed for the user. # @see #choice # @param name (see #choice) # @param help (see #choice) # @param action (see #choice) # @return (see #choice) def hidden(name, help = nil, &action) item = Menu::Item.new(name, text: name, help: help, action: action) @hidden_items << item @help.merge!(item.item_help) 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 or a literal String also adjusts # _index_suffix_ to a single space and _select_by_ to :name. # Because of this, you should make a habit of setting the _index_ first. # def index=(style) @index = style # Default settings. if @index == :none || @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. Mainly for internal use. # # @param topic [String] the menu item header/title/name to be associated with # a help message. # @param help [String] the help message to be associated with the menu # item/title/name. 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 TemplateRenderer # context so each method is properly delegated. # # 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 case @select_by when :index map_items_by_index when :name map_items_by_name else map_items_by_index + map_items_by_name end end def map_items_by_index if @index == :letter l_index = "`" all_items.map { l_index.succ!.dup } else (1..all_items.size).map(&:to_s) end end def map_items_by_name all_items.map(&:name) end def all_items @items + @hidden_items 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 {#choice}. # # @param highline_context [HighLine] a HighLine instance to be used as context. # @param selection [String, Integer] index or title of the selected menu item. # @param details additional parameter to be passed when in shell mode. # @return [nil, Object] if @nil_on_handled is set it returns +nil+, # else it returns the action return value. def select(highline_context, selection, details = nil) # add in any hidden menu commands items = all_items # Find the selected action. selected_item = find_item_from_selection(items, selection) # Run or return it. @highline = highline_context value_for_selected_item(selected_item, details) end def find_item_from_selection(items, selection) if selection =~ /^\d+$/ # is a number? get_item_by_number(items, selection) else get_item_by_letter(items, selection) end end # Returns the menu item referenced by its index # @param selection [Integer] menu item's index. def get_item_by_number(items, selection) items[selection.to_i - 1] end # Returns the menu item referenced by its title/header/name. # @param selection [String] menu's title/header/name def get_item_by_letter(items, selection) item = items.find { |i| i.name == selection } return item if item # 97 is the "a" letter at ascii table # Ex: For "a" it will return 0, and for "c" it will return 2 index = selection.ord - 97 items[index] end def value_for_selected_item(item, details) if item.action result = if @shell item.action.call(item.name, details) else item.action.call(item.name) end @nil_on_handled ? nil : result else item.name end end def gather_selected(highline_context, selections, details = nil) @highline = highline_context # add in any hidden menu commands items = all_items if selections.is_a?(Array) value_for_array_selections(items, selections, details) elsif selections.is_a?(Hash) value_for_hash_selections(items, selections, details) else raise ArgumentError, 'selections must be either Array or Hash' end end def value_for_array_selections(items, selections, details) # Find the selected items and return values selected_items = selections.map do |selection| find_item_from_selection(items, selection) end selected_items.map do |selected_item| value_for_selected_item(selected_item, details) end end def value_for_hash_selections(items, selections, details) # Find the selected items and return in hash form selections.each_with_object({}) do |(key, selection), memo| selected_item = find_item_from_selection(items, selection) memo[key] = value_for_selected_item(selected_item, details) end end def decorate_index(index) if index_color HighLine.color(index, index_color) else index end 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 @items.map.with_index { |item, ix| decorate_item(item.text.to_s, ix) } end def decorate_item(text, ix) decorated, non_decorated = mark_for_decoration(text, ix) decorate_index(decorated) + non_decorated end def mark_for_decoration(text, ix) case @index when :number ["#{ix + 1}#{@index_suffix}", text] when :letter ["#{('a'.ord + ix).chr}#{@index_suffix}", text] when :none [text, ""] else ["#{index}#{@index_suffix}", text] 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_s case @layout when :list %(<%= header ? "#{header}:\n" : '' %>) + parse_list + show_default_if_any + "<%= prompt %>" when :one_line %(<%= header ? "#{header}: " : '' %>) + "<%= prompt %>" \ "(" + parse_list + ")" + show_default_if_any + "<%= prompt[/\s*$/] %>" when :menu_only parse_list + show_default_if_any + "<%= prompt %>" else @layout end end def parse_list "<%= list( menu, #{@flow.inspect}, #{@list_option.inspect} ) %>" end def show_default_if_any default.to_s.empty? ? "" : "(#{default}) " end # # This method will update the intelligent responses to account for # Menu specific differences. Calls the superclass' (Question's) # build_responses method, overriding its default arguments to specify # 'options' will be used to populate choice lists. # def update_responses build_responses(options) end end end