Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/cli/ui/ansi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ def self.previous_line
def self.end_of_line
control("\033[", 'C')
end

def self.clear_to_end_of_line
control('', 'K')
end
end
end
end
65 changes: 58 additions & 7 deletions lib/cli/ui/prompt/interactive_options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def call
CLI::UI.raw { print(ANSI.hide_cursor) }
while @answer.nil?
render_options
wait_for_user_input
wait_for_actionable_user_input
reset_position
end
clear_output
Expand Down Expand Up @@ -74,14 +74,15 @@ def clear_output
end

def num_lines
options = presented_options.map(&:first)
# @options will be an array of questions but each option can be multi-line
# so to get the # of lines, you need to join then split

# empty_option_count is needed since empty option titles are omitted
# from the line count when reject(&:empty?) is called

empty_option_count = @options.count(&:empty?)
joined_options = @options.join("\n")
empty_option_count = options.count(&:empty?)
joined_options = options.join("\n")
joined_options.split("\n").reject(&:empty?).size + empty_option_count
end

Expand All @@ -107,6 +108,13 @@ def select_bool(char)
@answer = @options.index(opt) + 1
end

def wait_for_actionable_user_input
last_active = @active
while @active == last_active && @answer.nil?
wait_for_user_input
end
end

# rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon
def wait_for_user_input
char = read_char
Expand Down Expand Up @@ -157,12 +165,55 @@ def raw_tty!
end
end

def presented_options(recalculate: false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is really hard to follow.
I feel like it would do well to have some comments throughout explaining the code as it's quite logic dense and sort of magic with the [-2].last etc

return @presented_options unless recalculate

@presented_options = @options.zip(1..Float::INFINITY)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@options.each.with_index(1) is another way if you think it's more clear. I'm undecided.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change would need to_a as well (as you suggest is an emumerator which prevents the manipulations below). The zip seems clearer to me.

while num_lines > max_options
# 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
@presented_options.delete_at(-2)
else
# selection is closer to bottom than top, so trim a row from the top
ensure_first_item_is_continuation_marker
@presented_options.delete_at(1)
end
end

@presented_options
end

def distance_from_selection_to_end
last_visible_option_number = @presented_options[-1].last || @presented_options[-2].last
last_visible_option_number - @active
end

def distance_from_start_to_selection
first_visible_option_number = @presented_options[0].last || @presented_options[1].last
@active - first_visible_option_number
end

def ensure_last_item_is_continuation_marker
@presented_options.push(["...", nil]) if @presented_options.last.last
end

def ensure_first_item_is_continuation_marker
@presented_options.unshift(["...", nil]) if @presented_options.first.last
end


def max_options
@max_options ||= CLI::UI::Terminal.height - 2 # Keeps a one line question visible
end

def render_options
max_num_length = (@options.size + 1).to_s.length
@options.each_with_index do |choice, index|
num = index + 1

presented_options(recalculate: true).each do |choice, num|
padding = ' ' * (max_num_length - num.to_s.length)
message = " #{num}.#{padding}"
message = " #{num}#{num ? '.' : ' '}#{padding}"
message += choice.split("\n").map { |l| " {{bold:#{l}}}" }.join("\n")

if num == @active
Expand All @@ -172,7 +223,7 @@ def render_options
end

CLI::UI.with_frame_color(:blue) do
puts CLI::UI.fmt(message)
puts CLI::UI.fmt(message) + CLI::UI::ANSI.clear_to_end_of_line
end
end
end
Expand Down
12 changes: 12 additions & 0 deletions lib/cli/ui/terminal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module CLI
module UI
module Terminal
DEFAULT_WIDTH = 80
DEFAULT_HEIGHT = 24

# Returns the width of the terminal, if possible
# Otherwise will return 80
Expand All @@ -19,6 +20,17 @@ def self.width
rescue Errno::EIO
DEFAULT_WIDTH
end

def self.height
if console = IO.respond_to?(:console) && IO.console
height = console.winsize[0]
height.zero? ? DEFAULT_HEIGHT : height
else
DEFAULT_HEIGHT
end
rescue Errno::EIO
DEFAULT_HEIGHT
end
end
end
end
87 changes: 30 additions & 57 deletions test/cli/ui/prompt_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,8 @@ def test_confirm_sigint

expected_out = strip_heredoc(<<-EOF) + ' '
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. yes
2. no
\e[\e[C
> 1. yes
2. no
\e[?25l> 1. yes\e[K
2. no\e[K
\e[?25h\e[\e[C
EOF
assert_result(expected_out, "", :SIGINT)
Expand Down Expand Up @@ -77,11 +74,8 @@ def test_ask_interactive_sigint

expected_out = strip_heredoc(<<-EOF) + ' '
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. a
2. b
\e[\e[C
> 1. a
2. b
\e[?25l> 1. a\e[K
2. b\e[K
\e[?25h\e[\e[C
EOF
assert_result(expected_out, "", :SIGINT)
Expand All @@ -91,8 +85,8 @@ def test_confirm_happy_path
_run('y') { assert Prompt.confirm('q') }
expected_out = strip_heredoc(<<-EOF) + ' '
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. yes
2. no
\e[?25l> 1. yes\e[K
2. no\e[K
\e[\e[C
#{' ' * CLI::UI::Terminal.width}
#{' ' * CLI::UI::Terminal.width}
Expand All @@ -107,11 +101,8 @@ def test_confirm_invalid
_run(%w(r y n)) { Prompt.confirm('q') }
expected_out = strip_heredoc(<<-EOF) + ' '
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. yes
2. no
\e[\e[C
> 1. yes
2. no
\e[?25l> 1. yes\e[K
2. no\e[K
\e[\e[C
#{' ' * CLI::UI::Terminal.width}
#{' ' * CLI::UI::Terminal.width}
Expand All @@ -126,11 +117,8 @@ def test_confirm_no_match_internal
_run('x', 'n') { Prompt.confirm('q') }
expected_out = strip_heredoc(<<-EOF) + ' '
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. yes
2. no
\e[\e[C
> 1. yes
2. no
\e[?25l> 1. yes\e[K
2. no\e[K
\e[\e[C
#{' ' * CLI::UI::Terminal.width}
#{' ' * CLI::UI::Terminal.width}
Expand Down Expand Up @@ -229,8 +217,8 @@ def test_ask_interactive_with_block
end
expected_out = strip_heredoc(<<-EOF)
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. a
2. b
\e[?25l> 1. a\e[K
2. b\e[K
\e[\e[C
#{' ' * CLI::UI::Terminal.width}
#{' ' * CLI::UI::Terminal.width}
Expand All @@ -247,8 +235,8 @@ def test_ask_interactive_with_number
end
expected_out = strip_heredoc(<<-EOF)
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. a
2. b
\e[?25l> 1. a\e[K
2. b\e[K
\e[\e[C
#{' ' * CLI::UI::Terminal.width}
#{' ' * CLI::UI::Terminal.width}
Expand All @@ -265,11 +253,11 @@ def test_ask_interactive_with_vim_bound_arrows
end
expected_out = strip_heredoc(<<-EOF)
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. a
2. b
\e[?25l> 1. a\e[K
2. b\e[K
\e[\e[C
1. a
> 2. b
1. a\e[K
> 2. b\e[K
\e[\e[C
#{' ' * CLI::UI::Terminal.width}
#{' ' * CLI::UI::Terminal.width}
Expand All @@ -286,8 +274,8 @@ def test_ask_interactive_select_using_space
end
expected_out = strip_heredoc(<<-EOF)
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. a
2. b
\e[?25l> 1. a\e[K
2. b\e[K
\e[\e[C
#{' ' * CLI::UI::Terminal.width}
#{' ' * CLI::UI::Terminal.width}
Expand All @@ -309,11 +297,8 @@ def test_ask_interactive_escape

expected_out = strip_heredoc(<<-EOF)
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. a
2. b
\e[\e[C
> 1. a
2. b
\e[?25l> 1. a\e[K
2. b\e[K
\e[?25h\e[\e[C
EOF
assert_result(expected_out, nil, :SIGINT)
Expand All @@ -325,20 +310,8 @@ def test_ask_interactive_invalid_input
end
expected_out = strip_heredoc(<<-EOF)
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. a
2. b
\e[\e[C
> 1. a
2. b
\e[\e[C
> 1. a
2. b
\e[\e[C
> 1. a
2. b
\e[\e[C
> 1. a
2. b
\e[?25l> 1. a\e[K
2. b\e[K
\e[\e[C
#{' ' * CLI::UI::Terminal.width}
#{' ' * CLI::UI::Terminal.width}
Expand All @@ -359,14 +332,14 @@ def test_ask_interactive_with_blank_option
blank = ''
expected_out = strip_heredoc(<<-EOF)
? q (choose with ↑ ↓ ⏎)
\e[?25l> 1. a
2.#{blank}
\e[?25l> 1. a\e[K
2.#{blank}\e[K
\e[\e[C
1. a
> 2.#{blank}
1. a\e[K
> 2.#{blank}\e[K
\e[\e[C
> 1. a
2.#{blank}
> 1. a\e[K
2.#{blank}\e[K
\e[\e[C
#{' ' * CLI::UI::Terminal.width}
#{' ' * CLI::UI::Terminal.width}
Expand Down