class TTY::Prompt::List

A class responsible for rendering select list menu Used by {Prompt} to display interactive menu.

@api private

Constants

FILTER_KEYS_MATCHER

Allowed keys for filter, along with backspace and canc.

INTEGER_MATCHER

Checks type of default parameter to be integer

Public Class Methods

new(prompt, **options) click to toggle source

Create instance of TTY::Prompt::List menu.

@param Hash options

the configuration options

@option options [Symbol] :default

the default active choice, defaults to 1

@option options [Symbol] :color

the color for the selected item, defualts to :green

@option options [Symbol] :marker

the marker for the selected item

@option options [String] :enum

the delimiter for the item index

@api public

# File lib/tty/prompt/list.rb, line 36
def initialize(prompt, **options)
  check_options_consistency(options)

  @prompt       = prompt
  @prefix       = options.fetch(:prefix) { @prompt.prefix }
  @enum         = options.fetch(:enum) { nil }
  @default      = Array(options[:default])
  @choices      = Choices.new
  @active_color = options.fetch(:active_color) { @prompt.active_color }
  @help_color   = options.fetch(:help_color) { @prompt.help_color }
  @cycle        = options.fetch(:cycle) { false }
  @filterable   = options.fetch(:filter) { false }
  @symbols      = @prompt.symbols.merge(options.fetch(:symbols, {}))
  @quiet        = options.fetch(:quiet) { @prompt.quiet }
  @filter       = []
  @filter_cache = {}
  @help         = options[:help]
  @show_help    = options.fetch(:show_help) { :start }
  @first_render = true
  @done         = false
  @per_page     = options[:per_page]
  @paginator    = Paginator.new
  @block_paginator = BlockPaginator.new
  @by_page      = false
  @paging_changed = false
end

Public Instance Methods

arrows_help() click to toggle source

Information about arrow keys

@return [String]

@api private

# File lib/tty/prompt/list.rb, line 156
def arrows_help
  up_down = @symbols[:arrow_up] + "/" + @symbols[:arrow_down]
  left_right = @symbols[:arrow_left] + "/" + @symbols[:arrow_right]

  arrows = [up_down]
  arrows << "/" if paginated?
  arrows << left_right if paginated?
  arrows.join
end
call(question, possibilities, &block) click to toggle source

Call the list menu by passing question and choices

@param [String] question

@param @api public

# File lib/tty/prompt/list.rb, line 239
def call(question, possibilities, &block)
  choices(possibilities)
  @question = question
  block.call(self) if block
  setup_defaults
  @prompt.subscribe(self) do
    render
  end
end
choice(*value, &block) click to toggle source

Add a single choice

@api public

# File lib/tty/prompt/list.rb, line 201
def choice(*value, &block)
  @filter_cache = {}
  if block
    @choices << (value << block)
  else
    @choices << value
  end
end
choices(values = (not_set = true)) click to toggle source

Add multiple choices, or return them.

@param [Array] values

the values to add as choices; if not passed, the current
choices are displayed.

@api public

# File lib/tty/prompt/list.rb, line 217
def choices(values = (not_set = true))
  if not_set
    if !filterable? || @filter.empty?
      @choices
    else
      filter_value = @filter.join.downcase
      @filter_cache[filter_value] ||= @choices.enabled.select do |choice|
        choice.name.to_s.downcase.include?(filter_value)
      end
    end
  else
    @filter_cache = {}
    values.each { |val| @choices << val }
  end
end
default(*default_values) click to toggle source

Set default option selected

@api public

# File lib/tty/prompt/list.rb, line 78
def default(*default_values)
  @default = default_values
end
default_help() click to toggle source

Default help text

Note that enumeration and filter are mutually exclusive

@a public

# File lib/tty/prompt/list.rb, line 171
def default_help
  str = []
  str << "(Press "
  str << "#{arrows_help} arrow"
  str << " or 1-#{choices.size} number" if enumerate?
  str << " to move"
  str << (filterable? ? "," : " and")
  str << " Enter to select"
  str << " and letters to filter" if filterable?
  str << ")"
  str.join
end
enum(value) click to toggle source

Set selecting active index using number pad

@api public

# File lib/tty/prompt/list.rb, line 187
def enum(value)
  @enum = value
end
enumerate?() click to toggle source

Check if list is enumerated

@return [Boolean]

# File lib/tty/prompt/list.rb, line 252
def enumerate?
  !@enum.nil?
end
help(value = (not_set = true)) click to toggle source

Provide help information

@param [String] value

the new help text

@return [String]

@api public

# File lib/tty/prompt/list.rb, line 136
def help(value = (not_set = true))
  return @help if !@help.nil? && not_set

  @help = (@help.nil? && !not_set) ? value : default_help
end
keybackspace(*) click to toggle source
# File lib/tty/prompt/list.rb, line 367
def keybackspace(*)
  return unless filterable?

  @filter.pop
  @active = 1
end
keydelete(*) click to toggle source
# File lib/tty/prompt/list.rb, line 360
def keydelete(*)
  return unless filterable?

  @filter.clear
  @active = 1
end
keydown(*) click to toggle source
# File lib/tty/prompt/list.rb, line 293
def keydown(*)
  searchable  = ((@active + 1)..choices.length)
  next_active = search_choice_in(searchable)

  if next_active
    @active = next_active
  elsif @cycle
    searchable = (1..choices.length)
    next_active = search_choice_in(searchable)

    @active = next_active if next_active
  end
  @paging_changed = @by_page
  @by_page = false
end
Also aliased as: keytab
keyenter(*) click to toggle source
# File lib/tty/prompt/list.rb, line 266
def keyenter(*)
  @done = true unless choices.empty?
end
Also aliased as: keyreturn, keyspace
keyleft(*) click to toggle source
# File lib/tty/prompt/list.rb, line 338
def keyleft(*)
  if (@active - page_size) > 0
    searchable = ((@active - page_size)..choices.size)
    @active = search_choice_in(searchable)
  elsif @cycle
    searchable = choices.size.downto(1).to_a
    @active = search_choice_in(searchable)
  end
  @paging_changed = !@by_page
  @by_page = true
end
Also aliased as: keypage_up
keynum(event) click to toggle source
# File lib/tty/prompt/list.rb, line 256
def keynum(event)
  return unless enumerate?

  value = event.value.to_i
  return unless (1..choices.count).cover?(value)
  return if choices[value - 1].disabled?

  @active = value
end
keypage_down(*)
Alias for: keyright
keypage_up(*)
Alias for: keyleft
keypress(event) click to toggle source
# File lib/tty/prompt/list.rb, line 351
def keypress(event)
  return unless filterable?

  if event.value =~ FILTER_KEYS_MATCHER
    @filter << event.value
    @active = 1
  end
end
keyreturn(*)
Alias for: keyenter
keyright(*) click to toggle source

Moves all choices page by page keeping the current selected item at the same level on each page.

When the choice on a page is outside of next page range then adjust it to the last item, otherwise leave unchanged.

# File lib/tty/prompt/list.rb, line 315
def keyright(*)
  choices_size = choices.size
  if (@active + page_size) <= choices_size
    searchable = ((@active + page_size)..choices_size)
    @active = search_choice_in(searchable)
  elsif @active <= choices_size # last page shorter
    current   = @active % page_size
    remaining = choices_size % page_size

    if current.zero? || (remaining > 0 && current > remaining)
      searchable = choices_size.downto(0).to_a
      @active = search_choice_in(searchable)
    elsif @cycle
      searchable = ((current.zero? ? page_size : current)..choices_size)
      @active = search_choice_in(searchable)
    end
  end

  @paging_changed = !@by_page
  @by_page = true
end
Also aliased as: keypage_down
keyspace(*)
Alias for: keyenter
keytab(*)
Alias for: keydown
keyup(*) click to toggle source
# File lib/tty/prompt/list.rb, line 276
def keyup(*)
  searchable  = (@active - 1).downto(1).to_a
  prev_active = search_choice_in(searchable)

  if prev_active
    @active = prev_active
  elsif @cycle
    searchable  = choices.length.downto(1).to_a
    prev_active = search_choice_in(searchable)

    @active = prev_active if prev_active
  end

  @paging_changed = @by_page
  @by_page = false
end
page_size() click to toggle source
# File lib/tty/prompt/list.rb, line 115
def page_size
  (@per_page || Paginator::DEFAULT_PAGE_SIZE)
end
paginated?() click to toggle source

Check if list is paginated

@return [Boolean]

@api private

# File lib/tty/prompt/list.rb, line 124
def paginated?
  choices.size > page_size
end
paginator() click to toggle source

Select paginator based on the current navigation key

@return [Paginator]

@api private

# File lib/tty/prompt/list.rb, line 87
def paginator
  @by_page ? @block_paginator : @paginator
end
per_page(value) click to toggle source

Set number of items per page

@api public

# File lib/tty/prompt/list.rb, line 111
def per_page(value)
  @per_page = value
end
quiet(value) click to toggle source

Set whether selected answers are echoed

@api public

# File lib/tty/prompt/list.rb, line 194
def quiet(value)
  @quiet = value
end
search_choice_in(searchable) click to toggle source
# File lib/tty/prompt/list.rb, line 272
def search_choice_in(searchable)
  searchable.find { |i| !choices[i - 1].disabled? }
end
show_help(value = (not_set = true)) click to toggle source

Change when help is displayed

@api public

# File lib/tty/prompt/list.rb, line 145
def show_help(value = (not_set = true))
  return @show_ehlp if not_set

  @show_help = value
end
symbols(new_symbols = (not_set = true)) click to toggle source

Change symbols used by this prompt

@param [Hash] new_symbols

the new symbols to use

@api public

# File lib/tty/prompt/list.rb, line 69
def symbols(new_symbols = (not_set = true))
  return @symbols if not_set

  @symbols.merge!(new_symbols)
end
sync_paginators() click to toggle source

Synchronize paginators start positions

@api private

# File lib/tty/prompt/list.rb, line 94
def sync_paginators
  if @by_page
    if @paginator.start_index
      @block_paginator.reset!
      @block_paginator.start_index = @paginator.start_index
    end
  else
    if @block_paginator.start_index
      @paginator.reset!
      @paginator.start_index = @block_paginator.start_index
    end
  end
end

Private Instance Methods

answer() click to toggle source

Find value for the choice selected

@return [nil, Object]

@api private

# File lib/tty/prompt/list.rb, line 484
def answer
  choices[@active - 1].value
end
check_options_consistency(options) click to toggle source
# File lib/tty/prompt/list.rb, line 376
def check_options_consistency(options)
  if options.key?(:enum) && options.key?(:filter)
    raise ConfigurationError,
          "Enumeration can't be used with filter"
  end
end
filter_help() click to toggle source

Header part showing the current filter

@return String

@api private

# File lib/tty/prompt/list.rb, line 525
def filter_help
  "(Filter: #{@filter.join.inspect})"
end
filterable?() click to toggle source

Is filtering enabled?

@return [Boolean]

@api private

# File lib/tty/prompt/list.rb, line 516
def filterable?
  @filterable
end
help_always?() click to toggle source

Check if help is always displayed

@api private

# File lib/tty/prompt/list.rb, line 539
def help_always?
  @show_help =~ /always/i
end
help_start?() click to toggle source

Check if help is shown only on start

@api private

# File lib/tty/prompt/list.rb, line 532
def help_start?
  @show_help =~ /start/i
end
question_lines_count(question_lines) click to toggle source

Count how many screen lines the question spans

@return [Integer]

@api private

# File lib/tty/prompt/list.rb, line 473
def question_lines_count(question_lines)
  question_lines.reduce(0) do |acc, line|
    acc + @prompt.count_screen_lines(line)
  end
end
refresh(lines) click to toggle source

Clear screen lines

@param [String]

@api private

# File lib/tty/prompt/list.rb, line 493
def refresh(lines)
  @prompt.clear_lines(lines)
end
render() click to toggle source

Render a selection list.

By default the result is printed out.

@return [Object] value

return the selected value

@api private

# File lib/tty/prompt/list.rb, line 449
def render
  @prompt.print(@prompt.hide)
  until @done
    question = render_question
    @prompt.print(question)
    @prompt.read_keypress

    # Split manually; if the second line is blank (when there are no
    # matching lines), it won't be included by using String#lines.
    question_lines = question.split($INPUT_RECORD_SEPARATOR, -1)

    @prompt.print(refresh(question_lines_count(question_lines)))
  end
  @prompt.print(render_question) unless @quiet
  answer
ensure
  @prompt.print(@prompt.show)
end
render_header() click to toggle source

Render initial help and selected choice

@return [String]

@api private

# File lib/tty/prompt/list.rb, line 548
def render_header
  if @done
    selected_item = choices[@active - 1].name
    @prompt.decorate(selected_item.to_s, @active_color)
  elsif (@first_render && (help_start? || help_always?)) ||
        (help_always? && !@filter.any?)
    @prompt.decorate(help, @help_color)
  elsif filterable? && @filter.any?
    @prompt.decorate(filter_help, @help_color)
  end
end
render_menu() click to toggle source

Render menu with choices to select from

@return [String]

@api private

# File lib/tty/prompt/list.rb, line 565
def render_menu
  output = []

  sync_paginators if @paging_changed
  paginator.paginate(choices, @active, @per_page) do |choice, index|
    num = enumerate? ? (index + 1).to_s + @enum + " " : ""
    message = if index + 1 == @active && !choice.disabled?
                selected = "#{@symbols[:marker]} #{num}#{choice.name}"
                @prompt.decorate(selected.to_s, @active_color)
              elsif choice.disabled?
                @prompt.decorate(@symbols[:cross], :red) +
                  " #{num}#{choice.name} #{choice.disabled}"
              else
                "  #{num}#{choice.name}"
              end
    end_index = paginated? ? paginator.end_index : choices.size - 1
    newline = (index == end_index) ? "" : "\n"
    output << (message + newline)
  end

  output.join
end
render_question() click to toggle source

Render question with instructions and menu

@return [String]

@api private

# File lib/tty/prompt/list.rb, line 502
def render_question
  header = ["#{@prefix}#{@question} #{render_header}\n"]
  @first_render = false
  unless @done
    header << render_menu
  end
  header.join
end
setup_defaults() click to toggle source

Setup default option and active selection

@return [Integer]

@api private

# File lib/tty/prompt/list.rb, line 388
def setup_defaults
  validate_defaults

  if @default.empty?
    # no default, pick the first non-disabled choice
    @active = choices.index { |choice| !choice.disabled? } + 1
  elsif @default.first.to_s =~ INTEGER_MATCHER
    @active = @default.first
  elsif default_choice = choices.find_by(:name, @default.first)
    @active = choices.index(default_choice) + 1
  end
end
validate_default_name(name) click to toggle source

Validate default choice name

@param [String] name

the name to verify

@return [String]

@api private

# File lib/tty/prompt/list.rb, line 432
def validate_default_name(name)
  default_choice = choices.find_by(:name, name.to_s)
  if default_choice.nil?
    "no choice found for the default name: #{name.inspect}"
  elsif default_choice.disabled?
    "default name #{name.inspect} matches disabled choice"
  end
end
validate_defaults() click to toggle source

Validate default indexes to be within range

@raise [ConfigurationError]

raised when the default index is either non-integer,
out of range or clashes with disabled choice item.

@api private

# File lib/tty/prompt/list.rb, line 408
def validate_defaults
  @default.each do |d|
    msg = if d.nil? || d.to_s.empty?
            "default index must be an integer in range (1 - #{choices.size})"
          elsif d.to_s !~ INTEGER_MATCHER
            validate_default_name(d)
          elsif d < 1 || d > choices.size
            "default index `#{d}` out of range (1 - #{choices.size})"
          elsif (dflt_choice = choices[d - 1]) && dflt_choice.disabled?
            "default index `#{d}` matches disabled choice"
          end

    raise(ConfigurationError, msg) if msg
  end
end