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