1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578
|
# 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:
#
# <tt>:index</tt>:: The user is allowed to type the numerical
# or alphabetical index for their selection.
# <tt>:index_or_name</tt>:: Allows both methods from the
# <tt>:index</tt> option and the
# <tt>:name</tt> option.
# <tt>:name</tt>:: 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 <tt>@highline</tt> 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 configuration
# 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}. <b>Be
# warned:</b> 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:
#
# <tt>:number</tt>:: Menu items will be indexed numerically, starting
# with 1. This is the default method of indexing.
# <tt>:letter</tt>:: Items will be indexed alphabetically, starting
# with a.
# <tt>:none</tt>:: No index will be appended to menu items.
# <i>any String</i>:: Will be used as the literal _index_.
#
# Setting the _index_ to <tt>:none</tt> or a literal String also adjusts
# _index_suffix_ to a single space and _select_by_ to <tt>:name</tt>.
# Because of this, you should make a habit of setting the _index_ first.
#
def index=(style)
@index = style
return unless @index == :none || @index.is_a?(::String)
# Default settings.
@index_suffix = " "
@select_by = :name
end
#
# Initializes the help system by adding a <tt>:help</tt> choice, some
# action code, and the default help listing.
#
def init_help
return if @items.include?(:help)
topics = @help.keys.sort
help_help =
if @help.include?("help")
@help["help"]
else
"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) %>"
end
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 <tt>"help"</tt>
# 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:
#
# <tt>:list</tt>:: 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.
# <tt>:one_line</tt>:: 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.
# <tt>:menu_only</tt>:: Just the menu items, followed up by a likely
# short _prompt_.
# <i>any ERb String</i>:: Will be taken as the literal _layout_. This
# String can access <tt>header</tt>,
# <tt>menu</tt> and <tt>prompt</tt>, but is
# otherwise evaluated in the TemplateRenderer
# context so each method is properly delegated.
#
# If set to either <tt>:one_line</tt>, or <tt>:menu_only</tt>, _index_
# will default to <tt>:none</tt> and _flow_ will default to
# <tt>:inline</tt>.
#
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
size = all_items.size
case @index
when :letter
("a".."z").first(size)
when :capital_letter
("A".."Z").first(size)
else
(1..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.downcase.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
index = 0
selected_items.map do |selected_item|
value = value_for_selected_item(selected_item, self.shell ? details[index] : nil)
index += 1
value
end
end
def value_for_hash_selections(items, selections, details)
# Find the selected items and return in hash form
index = 0
selections.each_with_object({}) do |(key, selection), memo|
selected_item = find_item_from_selection(items, selection)
value = value_for_selected_item(selected_item, self.shell ? details[index] : nil)
index += 1
memo[key] = value
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, :capital_letter
first_letter = (@index == :capital_letter ? 'A' : 'a')
["#{(first_letter.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
|