cli-ui 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.dependabot/config.yml +8 -0
- data/.gitignore +0 -1
- data/.rubocop.yml +23 -2
- data/.travis.yml +4 -2
- data/Gemfile.lock +56 -0
- data/README.md +32 -1
- data/Rakefile +1 -1
- data/cli-ui.gemspec +3 -3
- data/dev.yml +1 -1
- data/lib/cli/ui.rb +58 -18
- data/lib/cli/ui/ansi.rb +9 -3
- data/lib/cli/ui/color.rb +9 -8
- data/lib/cli/ui/formatter.rb +13 -13
- data/lib/cli/ui/frame.rb +108 -151
- data/lib/cli/ui/frame/frame_stack.rb +98 -0
- data/lib/cli/ui/frame/frame_style.rb +120 -0
- data/lib/cli/ui/frame/frame_style/box.rb +166 -0
- data/lib/cli/ui/frame/frame_style/bracket.rb +139 -0
- data/lib/cli/ui/glyph.rb +21 -10
- data/lib/cli/ui/os.rb +63 -0
- data/lib/cli/ui/printer.rb +47 -0
- data/lib/cli/ui/progress.rb +9 -7
- data/lib/cli/ui/prompt.rb +50 -16
- data/lib/cli/ui/prompt/interactive_options.rb +63 -44
- data/lib/cli/ui/prompt/options_handler.rb +7 -2
- data/lib/cli/ui/spinner.rb +4 -6
- data/lib/cli/ui/spinner/spin_group.rb +18 -12
- data/lib/cli/ui/stdout_router.rb +12 -7
- data/lib/cli/ui/terminal.rb +26 -16
- data/lib/cli/ui/truncater.rb +3 -3
- data/lib/cli/ui/version.rb +1 -1
- data/lib/cli/ui/widgets.rb +2 -0
- metadata +16 -9
- data/lib/cli/ui/box.rb +0 -15
data/lib/cli/ui/os.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module CLI
|
2
|
+
module UI
|
3
|
+
module OS
|
4
|
+
# Determines which OS is currently running the UI, to make it easier to
|
5
|
+
# adapt its behaviour to the features of the OS.
|
6
|
+
def self.current
|
7
|
+
@current_os ||= case RUBY_PLATFORM
|
8
|
+
when /darwin/
|
9
|
+
Mac
|
10
|
+
when /linux/
|
11
|
+
Linux
|
12
|
+
when /mingw32/
|
13
|
+
Windows
|
14
|
+
else
|
15
|
+
raise "Could not determine OS from platform #{RUBY_PLATFORM}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Mac
|
20
|
+
class << self
|
21
|
+
def supports_emoji?
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def supports_color_prompt?
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def supports_arrow_keys?
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def shift_cursor_on_line_reset?
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Linux < Mac
|
40
|
+
end
|
41
|
+
|
42
|
+
class Windows
|
43
|
+
class << self
|
44
|
+
def supports_emoji?
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def supports_color_prompt?
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
def supports_arrow_keys?
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
def shift_cursor_on_line_reset?
|
57
|
+
true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'cli/ui'
|
2
|
+
|
3
|
+
module CLI
|
4
|
+
module UI
|
5
|
+
class Printer
|
6
|
+
# Print a message to a stream with common utilities.
|
7
|
+
# Allows overriding the color, encoding, and target stream.
|
8
|
+
# By default, it formats the string using CLI:UI and rescues common stream errors.
|
9
|
+
#
|
10
|
+
# ==== Attributes
|
11
|
+
#
|
12
|
+
# * +msg+ - (required) the string to output. Can be frozen.
|
13
|
+
#
|
14
|
+
# ==== Options
|
15
|
+
#
|
16
|
+
# * +:frame_color+ - Override the frame color. Defaults to nil.
|
17
|
+
# * +:to+ - Target stream, like $stdout or $stderr. Can be anything with a puts method. Defaults to $stdout.
|
18
|
+
# * +:encoding+ - Force the output to be in a certain encoding. Defaults to UTF-8.
|
19
|
+
# * +:format+ - Whether to format the string using CLI::UI.fmt. Defaults to true.
|
20
|
+
# * +:graceful+ - Whether to gracefully ignore common I/O errors. Defaults to true.
|
21
|
+
#
|
22
|
+
# ==== Returns
|
23
|
+
# Returns whether the message was successfully printed,
|
24
|
+
# which can be useful if +:graceful+ is set to true.
|
25
|
+
#
|
26
|
+
# ==== Example
|
27
|
+
#
|
28
|
+
# CLI::UI::Printer.puts('{x} Ouch', stream: $stderr, color: :red)
|
29
|
+
#
|
30
|
+
def self.puts(msg, frame_color: nil, to: $stdout, encoding: Encoding::UTF_8, format: true, graceful: true)
|
31
|
+
msg = (+msg).force_encoding(encoding) if encoding
|
32
|
+
msg = CLI::UI.fmt(msg) if format
|
33
|
+
|
34
|
+
if frame_color
|
35
|
+
CLI::UI::Frame.with_frame_color_override(frame_color) { to.puts(msg) }
|
36
|
+
else
|
37
|
+
to.puts(msg)
|
38
|
+
end
|
39
|
+
|
40
|
+
true
|
41
|
+
rescue Errno::EIO, Errno::EPIPE, IOError => e
|
42
|
+
raise(e) unless graceful
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/cli/ui/progress.rb
CHANGED
@@ -26,11 +26,11 @@ module CLI
|
|
26
26
|
#
|
27
27
|
# Increase the percent by X
|
28
28
|
# CLI::UI::Progress.progress do |bar|
|
29
|
-
# bar.tick(percent:
|
29
|
+
# bar.tick(percent: 0.05)
|
30
30
|
# end
|
31
31
|
def self.progress(width: Terminal.width)
|
32
32
|
bar = Progress.new(width: width)
|
33
|
-
print
|
33
|
+
print(CLI::UI::ANSI.hide_cursor)
|
34
34
|
yield(bar)
|
35
35
|
ensure
|
36
36
|
puts bar.to_s
|
@@ -59,14 +59,16 @@ module CLI
|
|
59
59
|
# * +:percent+ - Increment progress by a specific percent amount
|
60
60
|
# * +:set_percent+ - Set progress to a specific percent
|
61
61
|
#
|
62
|
+
# *Note:* The +:percent+ and +:set_percent must be between 0.00 and 1.0
|
63
|
+
#
|
62
64
|
def tick(percent: 0.01, set_percent: nil)
|
63
65
|
raise ArgumentError, 'percent and set_percent cannot both be specified' if percent != 0.01 && set_percent
|
64
66
|
@percent_done += percent
|
65
67
|
@percent_done = set_percent if set_percent
|
66
68
|
@percent_done = [@percent_done, 1.0].min # Make sure we can't go above 1.0
|
67
69
|
|
68
|
-
print
|
69
|
-
print
|
70
|
+
print(to_s)
|
71
|
+
print(CLI::UI::ANSI.previous_line + "\n")
|
70
72
|
end
|
71
73
|
|
72
74
|
# Format the progress bar to be printed to terminal
|
@@ -77,11 +79,11 @@ module CLI
|
|
77
79
|
filled = [(@percent_done * workable_width.to_f).ceil, 0].max
|
78
80
|
unfilled = [workable_width - filled, 0].max
|
79
81
|
|
80
|
-
CLI::UI.resolve_text
|
82
|
+
CLI::UI.resolve_text([
|
81
83
|
FILLED_BAR + ' ' * filled,
|
82
84
|
UNFILLED_BAR + ' ' * unfilled,
|
83
|
-
CLI::UI::Color::RESET.code + suffix
|
84
|
-
].join
|
85
|
+
CLI::UI::Color::RESET.code + suffix,
|
86
|
+
].join)
|
85
87
|
end
|
86
88
|
end
|
87
89
|
end
|
data/lib/cli/ui/prompt.rb
CHANGED
@@ -36,7 +36,8 @@ module CLI
|
|
36
36
|
# * +:select_ui+ - Enable long-form option selection (default: true)
|
37
37
|
#
|
38
38
|
# Note:
|
39
|
-
# * +:options+ or providing a +Block+ conflicts with +:default+ and +:is_file+,
|
39
|
+
# * +:options+ or providing a +Block+ conflicts with +:default+ and +:is_file+,
|
40
|
+
# you cannot set options with either of these keywords
|
40
41
|
# * +:default+ conflicts with +:allow_empty:, you cannot set these together
|
41
42
|
# * +:options+ conflicts with providing a +Block+ , you may only set one
|
42
43
|
# * +:multiple+ can only be used with +:options+ or a +Block+; it is ignored, otherwise.
|
@@ -49,7 +50,7 @@ module CLI
|
|
49
50
|
# ==== Return Value
|
50
51
|
#
|
51
52
|
# * If a +Block+ was not provided, the selected option or response to the free form question will be returned
|
52
|
-
# * If a +Block+ was provided, the
|
53
|
+
# * If a +Block+ was provided, the evaluated value of the +Block+ will be returned
|
53
54
|
#
|
54
55
|
# ==== Example Usage:
|
55
56
|
#
|
@@ -76,13 +77,35 @@ module CLI
|
|
76
77
|
# handler.option('python') { |selection| selection }
|
77
78
|
# end
|
78
79
|
#
|
79
|
-
def ask(
|
80
|
-
|
80
|
+
def ask(
|
81
|
+
question,
|
82
|
+
options: nil,
|
83
|
+
default: nil,
|
84
|
+
is_file: nil,
|
85
|
+
allow_empty: true,
|
86
|
+
multiple: false,
|
87
|
+
filter_ui: true,
|
88
|
+
select_ui: true,
|
89
|
+
&options_proc
|
90
|
+
)
|
91
|
+
if (options || block_given?) && ((default && !multiple) || is_file)
|
81
92
|
raise(ArgumentError, 'conflicting arguments: options provided with default or is_file')
|
82
93
|
end
|
83
94
|
|
95
|
+
if options && multiple && default && !(default - options).empty?
|
96
|
+
raise(ArgumentError, 'conflicting arguments: default should only include elements present in options')
|
97
|
+
end
|
98
|
+
|
84
99
|
if options || block_given?
|
85
|
-
ask_interactive(
|
100
|
+
ask_interactive(
|
101
|
+
question,
|
102
|
+
options,
|
103
|
+
multiple: multiple,
|
104
|
+
default: default,
|
105
|
+
filter_ui: filter_ui,
|
106
|
+
select_ui: select_ui,
|
107
|
+
&options_proc
|
108
|
+
)
|
86
109
|
else
|
87
110
|
ask_free_form(question, default, is_file, allow_empty)
|
88
111
|
end
|
@@ -132,7 +155,9 @@ module CLI
|
|
132
155
|
private
|
133
156
|
|
134
157
|
def ask_free_form(question, default, is_file, allow_empty)
|
135
|
-
|
158
|
+
if default && !allow_empty
|
159
|
+
raise(ArgumentError, 'conflicting arguments: default enabled but allow_empty is false')
|
160
|
+
end
|
136
161
|
|
137
162
|
if default
|
138
163
|
puts_question("#{question} (empty = #{default})")
|
@@ -155,7 +180,7 @@ module CLI
|
|
155
180
|
end
|
156
181
|
end
|
157
182
|
|
158
|
-
def ask_interactive(question, options = nil, multiple: false, filter_ui: true, select_ui: true)
|
183
|
+
def ask_interactive(question, options = nil, multiple: false, default: nil, filter_ui: true, select_ui: true)
|
159
184
|
raise(ArgumentError, 'conflicting arguments: options and block given') if options && block_given?
|
160
185
|
|
161
186
|
options ||= if block_given?
|
@@ -165,16 +190,22 @@ module CLI
|
|
165
190
|
end
|
166
191
|
|
167
192
|
raise(ArgumentError, 'insufficient options') if options.nil? || options.empty?
|
168
|
-
|
193
|
+
navigate_text = if CLI::UI::OS.current.supports_arrow_keys?
|
194
|
+
"Choose with ↑ ↓ ⏎"
|
195
|
+
else
|
196
|
+
"Navigate up with 'k' and down with 'j', press Enter to select"
|
197
|
+
end
|
198
|
+
|
199
|
+
instructions = (multiple ? "Toggle options. " : "") + navigate_text
|
169
200
|
instructions += ", filter with 'f'" if filter_ui
|
170
|
-
instructions += ", enter option with 'e'" if select_ui
|
201
|
+
instructions += ", enter option with 'e'" if select_ui && (options.size > 9)
|
171
202
|
puts_question("#{question} {{yellow:(#{instructions})}}")
|
172
|
-
resp = interactive_prompt(options, multiple: multiple)
|
203
|
+
resp = interactive_prompt(options, multiple: multiple, default: default)
|
173
204
|
|
174
205
|
# Clear the line
|
175
|
-
print
|
206
|
+
print(ANSI.previous_line + ANSI.clear_to_end_of_line)
|
176
207
|
# Force StdoutRouter to prefix
|
177
|
-
print
|
208
|
+
print(ANSI.previous_line + "\n")
|
178
209
|
|
179
210
|
# reset the question to include the answer
|
180
211
|
resp_text = resp
|
@@ -195,8 +226,8 @@ module CLI
|
|
195
226
|
end
|
196
227
|
|
197
228
|
# Useful for stubbing in tests
|
198
|
-
def interactive_prompt(options, multiple: false)
|
199
|
-
InteractiveOptions.call(options, multiple: multiple)
|
229
|
+
def interactive_prompt(options, multiple: false, default: nil)
|
230
|
+
InteractiveOptions.call(options, multiple: multiple, default: default)
|
200
231
|
end
|
201
232
|
|
202
233
|
def write_default_over_empty_input(default)
|
@@ -231,11 +262,14 @@ module CLI
|
|
231
262
|
# thread to manage output, but the current strategy feels like a
|
232
263
|
# better tradeoff.
|
233
264
|
prefix = CLI::UI.with_frame_color(:blue) { CLI::UI::Frame.prefix }
|
234
|
-
prompt
|
265
|
+
# If a prompt is interrupted on Windows it locks the colour of the terminal from that point on, so we should
|
266
|
+
# not change the colour here.
|
267
|
+
prompt = prefix + CLI::UI.fmt('{{blue:> }}')
|
268
|
+
prompt += CLI::UI::Color::YELLOW.code if CLI::UI::OS.current.supports_color_prompt?
|
235
269
|
|
236
270
|
begin
|
237
271
|
line = Readline.readline(prompt, true)
|
238
|
-
print
|
272
|
+
print(CLI::UI::Color::RESET.code)
|
239
273
|
line.to_s.chomp
|
240
274
|
rescue Interrupt
|
241
275
|
CLI::UI.raw { STDERR.puts('^C' + CLI::UI::Color::RESET.code) }
|
@@ -22,8 +22,8 @@ module CLI
|
|
22
22
|
# Ask an interactive question
|
23
23
|
# CLI::UI::Prompt::InteractiveOptions.call(%w(rails go python))
|
24
24
|
#
|
25
|
-
def self.call(options, multiple: false)
|
26
|
-
list = new(options, multiple: multiple)
|
25
|
+
def self.call(options, multiple: false, default: nil)
|
26
|
+
list = new(options, multiple: multiple, default: default)
|
27
27
|
selected = list.call
|
28
28
|
if multiple
|
29
29
|
selected.map { |s| options[s - 1] }
|
@@ -39,7 +39,7 @@ module CLI
|
|
39
39
|
#
|
40
40
|
# CLI::UI::Prompt::InteractiveOptions.new(%w(rails go python))
|
41
41
|
#
|
42
|
-
def initialize(options, multiple: false)
|
42
|
+
def initialize(options, multiple: false, default: nil)
|
43
43
|
@options = options
|
44
44
|
@active = 1
|
45
45
|
@marker = '>'
|
@@ -52,7 +52,13 @@ module CLI
|
|
52
52
|
@filter = ''
|
53
53
|
# 0-indexed array representing if selected
|
54
54
|
# @options[0] is selected if @chosen[0]
|
55
|
-
|
55
|
+
if multiple
|
56
|
+
@chosen = if default
|
57
|
+
@options.map { |option| default.include?(option) }
|
58
|
+
else
|
59
|
+
Array.new(@options.size) { false }
|
60
|
+
end
|
61
|
+
end
|
56
62
|
@redraw = true
|
57
63
|
@presented_options = []
|
58
64
|
end
|
@@ -95,16 +101,16 @@ module CLI
|
|
95
101
|
@option_lengths = @options.map do |text|
|
96
102
|
width = 1 if text.empty?
|
97
103
|
width ||= text
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
104
|
+
.split("\n")
|
105
|
+
.reject(&:empty?)
|
106
|
+
.map { |l| (CLI::UI.fmt(l, enable_color: false).length / max_width).ceil }
|
107
|
+
.reduce(&:+)
|
102
108
|
|
103
109
|
width
|
104
110
|
end
|
105
111
|
end
|
106
112
|
|
107
|
-
def reset_position(number_of_lines=num_lines)
|
113
|
+
def reset_position(number_of_lines = num_lines)
|
108
114
|
# This will put us back at the beginning of the options
|
109
115
|
# When we redraw the options, they will be overwritten
|
110
116
|
CLI::UI.raw do
|
@@ -112,12 +118,12 @@ module CLI
|
|
112
118
|
end
|
113
119
|
end
|
114
120
|
|
115
|
-
def clear_output(number_of_lines=num_lines)
|
121
|
+
def clear_output(number_of_lines = num_lines)
|
116
122
|
CLI::UI.raw do
|
117
123
|
# Write over all lines with whitespace
|
118
124
|
number_of_lines.times { puts(' ' * CLI::UI::Terminal.width) }
|
119
125
|
end
|
120
|
-
reset_position
|
126
|
+
reset_position(number_of_lines)
|
121
127
|
|
122
128
|
# Update if metadata is being displayed
|
123
129
|
# This must be done _after_ the output is cleared or it won't draw over
|
@@ -128,7 +134,7 @@ module CLI
|
|
128
134
|
# Don't use this in place of +@displaying_metadata+, this updates too
|
129
135
|
# quickly to be useful when drawing to the screen.
|
130
136
|
def display_metadata?
|
131
|
-
filtering?
|
137
|
+
filtering? || selecting? || has_filter?
|
132
138
|
end
|
133
139
|
|
134
140
|
def num_lines
|
@@ -136,7 +142,7 @@ module CLI
|
|
136
142
|
|
137
143
|
option_length = presented_options.reduce(0) do |total_length, (_, option_number)|
|
138
144
|
# Handle continuation markers and "Done" option when multiple is true
|
139
|
-
next total_length + 1 if option_number.nil?
|
145
|
+
next total_length + 1 if option_number.nil? || option_number.zero?
|
140
146
|
total_length + @option_lengths[option_number - 1]
|
141
147
|
end
|
142
148
|
|
@@ -153,7 +159,7 @@ module CLI
|
|
153
159
|
CTRL_D = "\u0004"
|
154
160
|
|
155
161
|
def up
|
156
|
-
active_index = @filtered_options.index { |_,num| num == @active } || 0
|
162
|
+
active_index = @filtered_options.index { |_, num| num == @active } || 0
|
157
163
|
|
158
164
|
previous_visible = @filtered_options[active_index - 1]
|
159
165
|
previous_visible ||= @filtered_options.last
|
@@ -163,7 +169,7 @@ module CLI
|
|
163
169
|
end
|
164
170
|
|
165
171
|
def down
|
166
|
-
active_index = @filtered_options.index { |_,num| num == @active } || 0
|
172
|
+
active_index = @filtered_options.index { |_, num| num == @active } || 0
|
167
173
|
|
168
174
|
next_visible = @filtered_options[active_index + 1]
|
169
175
|
next_visible ||= @filtered_options.first
|
@@ -216,7 +222,7 @@ module CLI
|
|
216
222
|
@redraw = true
|
217
223
|
|
218
224
|
# Control+D or Backspace on empty search closes search
|
219
|
-
if char == CTRL_D
|
225
|
+
if (char == CTRL_D) || (@filter.empty? && (char == BACKSPACE))
|
220
226
|
@filter = ''
|
221
227
|
@state = :root
|
222
228
|
return
|
@@ -231,7 +237,7 @@ module CLI
|
|
231
237
|
|
232
238
|
def select_current
|
233
239
|
# Prevent selection of invisible options
|
234
|
-
return unless presented_options.any? { |_,num| num == @active }
|
240
|
+
return unless presented_options.any? { |_, num| num == @active }
|
235
241
|
select_n(@active)
|
236
242
|
end
|
237
243
|
|
@@ -240,7 +246,7 @@ module CLI
|
|
240
246
|
wait_for_user_input until @redraw
|
241
247
|
end
|
242
248
|
|
243
|
-
# rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon
|
249
|
+
# rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon,Style/Semicolon
|
244
250
|
def wait_for_user_input
|
245
251
|
char = read_char
|
246
252
|
@last_char = char
|
@@ -250,22 +256,24 @@ module CLI
|
|
250
256
|
when CTRL_C ; raise Interrupt
|
251
257
|
end
|
252
258
|
|
259
|
+
max_digit = [@options.size, 9].min.to_s
|
253
260
|
case @state
|
254
261
|
when :root
|
255
262
|
case char
|
256
|
-
when ESC
|
257
|
-
when 'k'
|
258
|
-
when 'j'
|
259
|
-
when 'e', ':', 'G'
|
260
|
-
when 'f', '/'
|
261
|
-
when ('0'
|
262
|
-
when 'y', 'n'
|
263
|
-
when " ", "\r", "\n"
|
263
|
+
when ESC ; @state = :esc
|
264
|
+
when 'k' ; up
|
265
|
+
when 'j' ; down
|
266
|
+
when 'e', ':', 'G' ; start_line_select
|
267
|
+
when 'f', '/' ; start_filter
|
268
|
+
when ('0'..max_digit) ; select_n(char.to_i)
|
269
|
+
when 'y', 'n' ; select_bool(char)
|
270
|
+
when " ", "\r", "\n" ; select_current # <enter>
|
264
271
|
end
|
265
272
|
when :filter
|
266
273
|
case char
|
267
274
|
when ESC ; @state = :esc
|
268
275
|
when "\r", "\n" ; select_current
|
276
|
+
when "\b" ; update_search(BACKSPACE) # Happens on Windows
|
269
277
|
else ; update_search(char)
|
270
278
|
end
|
271
279
|
when :line_select
|
@@ -273,9 +281,9 @@ module CLI
|
|
273
281
|
when ESC ; @state = :esc
|
274
282
|
when 'k' ; up ; @state = :root
|
275
283
|
when 'j' ; down ; @state = :root
|
276
|
-
when 'e',':','G','q' ; stop_line_select
|
284
|
+
when 'e', ':', 'G', 'q' ; stop_line_select
|
277
285
|
when '0'..'9' ; build_selection(char)
|
278
|
-
when BACKSPACE ; chop_selection
|
286
|
+
when BACKSPACE ; chop_selection # Pop last input on backspace
|
279
287
|
when ' ', "\r", "\n" ; select_current
|
280
288
|
end
|
281
289
|
when :esc
|
@@ -347,16 +355,27 @@ module CLI
|
|
347
355
|
|
348
356
|
@presented_options = @options.zip(1..Float::INFINITY)
|
349
357
|
if has_filter?
|
350
|
-
@presented_options.select! { |option,_| option.downcase.include?(@filter.downcase) }
|
358
|
+
@presented_options.select! { |option, _| option.downcase.include?(@filter.downcase) }
|
351
359
|
end
|
352
360
|
|
353
361
|
# Used for selection purposes
|
362
|
+
@presented_options.push([DONE, 0]) if @multiple
|
354
363
|
@filtered_options = @presented_options.dup
|
355
364
|
|
356
|
-
@presented_options.unshift([DONE, 0]) if @multiple
|
357
|
-
|
358
365
|
ensure_visible_is_active if has_filter?
|
359
366
|
|
367
|
+
# Must have more lines before the selection than we can display
|
368
|
+
if distance_from_start_to_selection > max_lines
|
369
|
+
@presented_options.shift(distance_from_start_to_selection - max_lines)
|
370
|
+
ensure_first_item_is_continuation_marker
|
371
|
+
end
|
372
|
+
|
373
|
+
# Must have more lines after the selection than we can display
|
374
|
+
if distance_from_selection_to_end > max_lines
|
375
|
+
@presented_options.pop(distance_from_selection_to_end - max_lines)
|
376
|
+
ensure_last_item_is_continuation_marker
|
377
|
+
end
|
378
|
+
|
360
379
|
while num_lines > max_lines
|
361
380
|
# try to keep the selection centered in the window:
|
362
381
|
if distance_from_selection_to_end > distance_from_start_to_selection
|
@@ -388,7 +407,7 @@ module CLI
|
|
388
407
|
end
|
389
408
|
|
390
409
|
def index_of_active_option
|
391
|
-
@presented_options.index { |_,num| num == @active }.to_i
|
410
|
+
@presented_options.index { |_, num| num == @active }.to_i
|
392
411
|
end
|
393
412
|
|
394
413
|
def ensure_last_item_is_continuation_marker
|
@@ -415,14 +434,14 @@ module CLI
|
|
415
434
|
max_num_length = (@options.size + 1).to_s.length
|
416
435
|
|
417
436
|
metadata_text = if selecting?
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
437
|
+
select_text = @active
|
438
|
+
select_text = '{{info:e, q, or up/down anytime to exit}}' if @active == 0
|
439
|
+
"Select: #{select_text}"
|
440
|
+
elsif filtering? || has_filter?
|
441
|
+
filter_text = @filter
|
442
|
+
filter_text = '{{info:Ctrl-D anytime or Backspace now to exit}}' if @filter.empty?
|
443
|
+
"Filter: #{filter_text}"
|
444
|
+
end
|
426
445
|
|
427
446
|
if metadata_text
|
428
447
|
CLI::UI.with_frame_color(:blue) do
|
@@ -431,7 +450,7 @@ module CLI
|
|
431
450
|
end
|
432
451
|
|
433
452
|
options.each do |choice, num|
|
434
|
-
is_chosen = @multiple && num && @chosen[num - 1]
|
453
|
+
is_chosen = @multiple && num && @chosen[num - 1] && num != 0
|
435
454
|
|
436
455
|
padding = ' ' * (max_num_length - num.to_s.length)
|
437
456
|
message = " #{num}#{num ? '.' : ' '}#{padding}"
|
@@ -442,12 +461,12 @@ module CLI
|
|
442
461
|
format = "{{cyan:#{format}}}" if @multiple && is_chosen && num != @active
|
443
462
|
format = " #{format}"
|
444
463
|
|
445
|
-
message +=
|
464
|
+
message += format(format, CHECKBOX_ICON[is_chosen]) if @multiple && num && num > 0
|
446
465
|
message += format_choice(format, choice)
|
447
466
|
|
448
467
|
if num == @active
|
449
468
|
|
450
|
-
color =
|
469
|
+
color = filtering? || selecting? ? 'green' : 'blue'
|
451
470
|
message = message.split("\n").map { |l| "{{#{color}:> #{l.strip}}}" }.join("\n")
|
452
471
|
end
|
453
472
|
|
@@ -463,7 +482,7 @@ module CLI
|
|
463
482
|
|
464
483
|
return eol if lines.empty? # Handle blank options
|
465
484
|
|
466
|
-
lines.map! { |l|
|
485
|
+
lines.map! { |l| format(format, l) + eol }
|
467
486
|
lines.join("\n")
|
468
487
|
end
|
469
488
|
end
|