lib/cli/ui/prompt/interactive_options.rb in cli-ui-1.3.0 vs lib/cli/ui/prompt/interactive_options.rb in cli-ui-1.4.0
- old
+ new
@@ -20,12 +20,12 @@
# ==== Example Usage:
#
# Ask an interactive question
# CLI::UI::Prompt::InteractiveOptions.call(%w(rails go python))
#
- def self.call(options, multiple: false)
- list = new(options, multiple: multiple)
+ def self.call(options, multiple: false, default: nil)
+ list = new(options, multiple: multiple, default: default)
selected = list.call
if multiple
selected.map { |s| options[s - 1] }
else
options[selected - 1]
@@ -37,11 +37,11 @@
#
# ==== Example Usage:
#
# CLI::UI::Prompt::InteractiveOptions.new(%w(rails go python))
#
- def initialize(options, multiple: false)
+ def initialize(options, multiple: false, default: nil)
@options = options
@active = 1
@marker = '>'
@answer = nil
@state = :root
@@ -50,11 +50,17 @@
# the terminal output should be drawn over when processing user input
@displaying_metadata = false
@filter = ''
# 0-indexed array representing if selected
# @options[0] is selected if @chosen[0]
- @chosen = Array.new(@options.size) { false } if multiple
+ if multiple
+ @chosen = if default
+ @options.map { |option| default.include?(option) }
+ else
+ Array.new(@options.size) { false }
+ end
+ end
@redraw = true
@presented_options = []
end
# Calls the +InteractiveOptions+ and asks the question
@@ -93,52 +99,52 @@
).to_f
@option_lengths = @options.map do |text|
width = 1 if text.empty?
width ||= text
- .split("\n")
- .reject(&:empty?)
- .map { |l| (l.length / max_width).ceil }
- .reduce(&:+)
+ .split("\n")
+ .reject(&:empty?)
+ .map { |l| (CLI::UI.fmt(l, enable_color: false).length / max_width).ceil }
+ .reduce(&:+)
width
end
end
- def reset_position(number_of_lines=num_lines)
+ def reset_position(number_of_lines = num_lines)
# This will put us back at the beginning of the options
# When we redraw the options, they will be overwritten
CLI::UI.raw do
number_of_lines.times { print(ANSI.previous_line) }
end
end
- def clear_output(number_of_lines=num_lines)
+ def clear_output(number_of_lines = num_lines)
CLI::UI.raw do
# Write over all lines with whitespace
number_of_lines.times { puts(' ' * CLI::UI::Terminal.width) }
end
- reset_position number_of_lines
+ reset_position(number_of_lines)
# Update if metadata is being displayed
# This must be done _after_ the output is cleared or it won't draw over
# the entire output
@displaying_metadata = display_metadata?
end
# Don't use this in place of +@displaying_metadata+, this updates too
# quickly to be useful when drawing to the screen.
def display_metadata?
- filtering? or selecting? or has_filter?
+ filtering? || selecting? || has_filter?
end
def num_lines
calculate_option_line_lengths if terminal_width_changed?
option_length = presented_options.reduce(0) do |total_length, (_, option_number)|
# Handle continuation markers and "Done" option when multiple is true
- next total_length + 1 if option_number.nil? or option_number.zero?
+ next total_length + 1 if option_number.nil? || option_number.zero?
total_length + @option_lengths[option_number - 1]
end
option_length + (@displaying_metadata ? 1 : 0)
end
@@ -151,21 +157,21 @@
BACKSPACE = "\u007F"
CTRL_C = "\u0003"
CTRL_D = "\u0004"
def up
- active_index = @filtered_options.index { |_,num| num == @active } || 0
+ active_index = @filtered_options.index { |_, num| num == @active } || 0
previous_visible = @filtered_options[active_index - 1]
previous_visible ||= @filtered_options.last
@active = previous_visible ? previous_visible.last : -1
@redraw = true
end
def down
- active_index = @filtered_options.index { |_,num| num == @active } || 0
+ active_index = @filtered_options.index { |_, num| num == @active } || 0
next_visible = @filtered_options[active_index + 1]
next_visible ||= @filtered_options.first
@active = next_visible ? next_visible.last : -1
@@ -214,11 +220,11 @@
def update_search(char)
@redraw = true
# Control+D or Backspace on empty search closes search
- if char == CTRL_D or (@filter.empty? and char == BACKSPACE)
+ if (char == CTRL_D) || (@filter.empty? && (char == BACKSPACE))
@filter = ''
@state = :root
return
end
@@ -229,55 +235,57 @@
end
end
def select_current
# Prevent selection of invisible options
- return unless presented_options.any? { |_,num| num == @active }
+ return unless presented_options.any? { |_, num| num == @active }
select_n(@active)
end
def process_input_until_redraw_required
@redraw = false
wait_for_user_input until @redraw
end
- # rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon
+ # rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon,Style/Semicolon
def wait_for_user_input
char = read_char
@last_char = char
case char
when :timeout ; raise Interrupt # Timeout, use interrupt to simulate
when CTRL_C ; raise Interrupt
end
+ max_digit = [@options.size, 9].min.to_s
case @state
when :root
case char
- when ESC ; @state = :esc
- when 'k' ; up
- when 'j' ; down
- when 'e', ':', 'G' ; start_line_select
- when 'f', '/' ; start_filter
- when ('0'..@options.size.to_s) ; select_n(char.to_i)
- when 'y', 'n' ; select_bool(char)
- when " ", "\r", "\n" ; select_current # <enter>
+ when ESC ; @state = :esc
+ when 'k' ; up
+ when 'j' ; down
+ when 'e', ':', 'G' ; start_line_select
+ when 'f', '/' ; start_filter
+ when ('0'..max_digit) ; select_n(char.to_i)
+ when 'y', 'n' ; select_bool(char)
+ when " ", "\r", "\n" ; select_current # <enter>
end
when :filter
case char
when ESC ; @state = :esc
when "\r", "\n" ; select_current
+ when "\b" ; update_search(BACKSPACE) # Happens on Windows
else ; update_search(char)
end
when :line_select
case char
when ESC ; @state = :esc
when 'k' ; up ; @state = :root
when 'j' ; down ; @state = :root
- when 'e',':','G','q' ; stop_line_select
+ when 'e', ':', 'G', 'q' ; stop_line_select
when '0'..'9' ; build_selection(char)
- when BACKSPACE ; chop_selection # Pop last input on backspace
+ when BACKSPACE ; chop_selection # Pop last input on backspace
when ' ', "\r", "\n" ; select_current
end
when :esc
case char
when '[' ; @state = :esc_bracket
@@ -345,20 +353,31 @@
def presented_options(recalculate: false)
return @presented_options unless recalculate
@presented_options = @options.zip(1..Float::INFINITY)
if has_filter?
- @presented_options.select! { |option,_| option.downcase.include?(@filter.downcase) }
+ @presented_options.select! { |option, _| option.downcase.include?(@filter.downcase) }
end
# Used for selection purposes
+ @presented_options.push([DONE, 0]) if @multiple
@filtered_options = @presented_options.dup
- @presented_options.unshift([DONE, 0]) if @multiple
-
ensure_visible_is_active if has_filter?
+ # Must have more lines before the selection than we can display
+ if distance_from_start_to_selection > max_lines
+ @presented_options.shift(distance_from_start_to_selection - max_lines)
+ ensure_first_item_is_continuation_marker
+ end
+
+ # Must have more lines after the selection than we can display
+ if distance_from_selection_to_end > max_lines
+ @presented_options.pop(distance_from_selection_to_end - max_lines)
+ ensure_last_item_is_continuation_marker
+ end
+
while num_lines > max_lines
# try to keep the selection centered in the window:
if distance_from_selection_to_end > distance_from_start_to_selection
# selection is closer to top than bottom, so trim a row from the bottom
ensure_last_item_is_continuation_marker
@@ -386,11 +405,11 @@
def distance_from_start_to_selection
index_of_active_option
end
def index_of_active_option
- @presented_options.index { |_,num| num == @active }.to_i
+ @presented_options.index { |_, num| num == @active }.to_i
end
def ensure_last_item_is_continuation_marker
@presented_options.push(["...", nil]) if @presented_options.last.last
end
@@ -413,43 +432,43 @@
clear_output(previously_displayed_lines) if previously_displayed_lines > num_lines
max_num_length = (@options.size + 1).to_s.length
metadata_text = if selecting?
- select_text = @active
- select_text = '{{info:e, q, or up/down anytime to exit}}' if @active == 0
- "Select: #{select_text}"
- elsif filtering? or has_filter?
- filter_text = @filter
- filter_text = '{{info:Ctrl-D anytime or Backspace now to exit}}' if @filter.empty?
- "Filter: #{filter_text}"
- end
+ select_text = @active
+ select_text = '{{info:e, q, or up/down anytime to exit}}' if @active == 0
+ "Select: #{select_text}"
+ elsif filtering? || has_filter?
+ filter_text = @filter
+ filter_text = '{{info:Ctrl-D anytime or Backspace now to exit}}' if @filter.empty?
+ "Filter: #{filter_text}"
+ end
if metadata_text
CLI::UI.with_frame_color(:blue) do
puts CLI::UI.fmt(" {{green:#{metadata_text}}}#{ANSI.clear_to_end_of_line}")
end
end
options.each do |choice, num|
- is_chosen = @multiple && num && @chosen[num - 1]
+ is_chosen = @multiple && num && @chosen[num - 1] && num != 0
padding = ' ' * (max_num_length - num.to_s.length)
message = " #{num}#{num ? '.' : ' '}#{padding}"
format = "%s"
# If multiple, bold only selected. If not multiple, bold everything
format = "{{bold:#{format}}}" if !@multiple || is_chosen
format = "{{cyan:#{format}}}" if @multiple && is_chosen && num != @active
format = " #{format}"
- message += sprintf(format, CHECKBOX_ICON[is_chosen]) if @multiple && num && num > 0
+ message += format(format, CHECKBOX_ICON[is_chosen]) if @multiple && num && num > 0
message += format_choice(format, choice)
if num == @active
- color = (filtering? or selecting?) ? 'green' : 'blue'
+ color = filtering? || selecting? ? 'green' : 'blue'
message = message.split("\n").map { |l| "{{#{color}:> #{l.strip}}}" }.join("\n")
end
CLI::UI.with_frame_color(:blue) do
puts CLI::UI.fmt(message)
@@ -461,10 +480,10 @@
eol = CLI::UI::ANSI.clear_to_end_of_line
lines = choice.split("\n")
return eol if lines.empty? # Handle blank options
- lines.map! { |l| sprintf(format, l) + eol }
+ lines.map! { |l| format(format, l) + eol }
lines.join("\n")
end
end
end
end