tty-prompt 0.15.0 → 0.16.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 +5 -5
- data/.travis.yml +1 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +2 -3
- data/README.md +140 -12
- data/appveyor.yml +1 -0
- data/examples/enum_select_disabled.rb +16 -0
- data/examples/{enum_paged.rb → enum_select_paged.rb} +0 -0
- data/examples/enum_select_wrapped.rb +15 -0
- data/examples/multi_select_disabled.rb +17 -0
- data/examples/multi_select_wrapped.rb +15 -0
- data/examples/select.rb +3 -1
- data/examples/select_disabled.rb +18 -0
- data/examples/{enum.rb → select_enum.rb} +0 -0
- data/examples/select_filtered.rb +3 -1
- data/examples/select_paginated.rb +3 -1
- data/examples/select_wrapped.rb +15 -0
- data/lib/tty/prompt.rb +1 -0
- data/lib/tty/prompt/answers_collector.rb +1 -0
- data/lib/tty/prompt/choice.rb +67 -25
- data/lib/tty/prompt/choices.rb +1 -0
- data/lib/tty/prompt/confirm_question.rb +3 -4
- data/lib/tty/prompt/converter_dsl.rb +1 -0
- data/lib/tty/prompt/converter_registry.rb +1 -0
- data/lib/tty/prompt/converters.rb +1 -0
- data/lib/tty/prompt/distance.rb +1 -0
- data/lib/tty/prompt/enum_list.rb +58 -17
- data/lib/tty/prompt/enum_paginator.rb +1 -0
- data/lib/tty/prompt/evaluator.rb +1 -0
- data/lib/tty/prompt/expander.rb +14 -14
- data/lib/tty/prompt/keypress.rb +1 -1
- data/lib/tty/prompt/list.rb +78 -39
- data/lib/tty/prompt/mask_question.rb +5 -4
- data/lib/tty/prompt/multi_list.rb +13 -3
- data/lib/tty/prompt/multiline.rb +6 -5
- data/lib/tty/prompt/paginator.rb +1 -0
- data/lib/tty/prompt/question.rb +10 -9
- data/lib/tty/prompt/question/checks.rb +1 -0
- data/lib/tty/prompt/question/modifier.rb +1 -0
- data/lib/tty/prompt/question/validation.rb +1 -0
- data/lib/tty/prompt/result.rb +1 -0
- data/lib/tty/prompt/slider.rb +3 -2
- data/lib/tty/prompt/statement.rb +1 -0
- data/lib/tty/prompt/suggestion.rb +4 -6
- data/lib/tty/prompt/symbols.rb +2 -1
- data/lib/tty/prompt/timeout.rb +16 -11
- data/lib/tty/prompt/utils.rb +1 -0
- data/lib/tty/prompt/version.rb +1 -1
- data/lib/tty/test_prompt.rb +1 -0
- metadata +11 -5
data/lib/tty/prompt/choices.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require_relative 'question'
|
4
5
|
require_relative 'utils'
|
@@ -112,10 +113,8 @@ module TTY
|
|
112
113
|
|
113
114
|
# @api private
|
114
115
|
def create_suffix
|
115
|
-
|
116
|
-
|
117
|
-
result << '/'
|
118
|
-
result << "#{default ? negative.downcase : negative.capitalize}"
|
116
|
+
(default ? positive.capitalize : positive.downcase) + '/' +
|
117
|
+
(default ? negative.downcase : negative.capitalize)
|
119
118
|
end
|
120
119
|
|
121
120
|
# Create custom conversion
|
data/lib/tty/prompt/distance.rb
CHANGED
data/lib/tty/prompt/enum_list.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'English'
|
2
5
|
|
3
6
|
require_relative 'choices'
|
4
7
|
require_relative 'enum_paginator'
|
5
8
|
require_relative 'paginator'
|
9
|
+
require_relative 'symbols'
|
6
10
|
|
7
11
|
module TTY
|
8
12
|
class Prompt
|
@@ -11,7 +15,9 @@ module TTY
|
|
11
15
|
#
|
12
16
|
# @api private
|
13
17
|
class EnumList
|
14
|
-
|
18
|
+
include Symbols
|
19
|
+
|
20
|
+
PAGE_HELP = '(Press tab/right or left to reveal more choices)'
|
15
21
|
|
16
22
|
# Create instance of EnumList menu.
|
17
23
|
#
|
@@ -97,8 +103,12 @@ module TTY
|
|
97
103
|
# the values to add as choices
|
98
104
|
#
|
99
105
|
# @api public
|
100
|
-
def choices(values)
|
101
|
-
|
106
|
+
def choices(values = (not_set = true))
|
107
|
+
if not_set
|
108
|
+
@choices
|
109
|
+
else
|
110
|
+
values.each { |val| @choices << val }
|
111
|
+
end
|
102
112
|
end
|
103
113
|
|
104
114
|
# Call the list menu by passing question and choices
|
@@ -128,7 +138,11 @@ module TTY
|
|
128
138
|
|
129
139
|
def keyreturn(*)
|
130
140
|
@failure = false
|
131
|
-
|
141
|
+
num = @input.to_i
|
142
|
+
choice_disabled = choices[num - 1] && choices[num - 1].disabled?
|
143
|
+
choice_in_range = num > 0 && num <= @choices.size
|
144
|
+
|
145
|
+
if choice_in_range && !choice_disabled || @input.empty?
|
132
146
|
@done = true
|
133
147
|
else
|
134
148
|
@input = ''
|
@@ -162,7 +176,11 @@ module TTY
|
|
162
176
|
#
|
163
177
|
# @api private
|
164
178
|
def mark_choice_as_active
|
165
|
-
|
179
|
+
next_active = @choices[@input.to_i - 1]
|
180
|
+
|
181
|
+
if next_active && next_active.disabled?
|
182
|
+
# noop
|
183
|
+
elsif (@input.to_i > 0) && next_active
|
166
184
|
@active = @input.to_i
|
167
185
|
else
|
168
186
|
@active = @default
|
@@ -174,9 +192,15 @@ module TTY
|
|
174
192
|
#
|
175
193
|
# @api private
|
176
194
|
def validate_defaults
|
177
|
-
|
178
|
-
|
179
|
-
|
195
|
+
msg = if @default.nil? || @default.to_s.empty?
|
196
|
+
"default index must be an integer in range (1 - #{choices.size})"
|
197
|
+
elsif @default < 1 || @default > @choices.size
|
198
|
+
"default index #{@default} out of range (1 - #{@choices.size})"
|
199
|
+
elsif choices[@default - 1] && choices[@default - 1].disabled?
|
200
|
+
"default index #{@default} matches disabled choice item"
|
201
|
+
end
|
202
|
+
|
203
|
+
raise(ConfigurationError, msg) if msg
|
180
204
|
end
|
181
205
|
|
182
206
|
# Setup default option and active selection
|
@@ -205,12 +229,24 @@ module TTY
|
|
205
229
|
@prompt.print(render_page_help)
|
206
230
|
end
|
207
231
|
@prompt.read_keypress
|
208
|
-
|
232
|
+
question_lines = question.split($INPUT_RECORD_SEPARATOR, -1)
|
233
|
+
@prompt.print(refresh(question_lines_count(question_lines)))
|
209
234
|
end
|
210
235
|
@prompt.print(render_question)
|
211
236
|
answer
|
212
237
|
end
|
213
238
|
|
239
|
+
# Count how many screen lines the question spans
|
240
|
+
#
|
241
|
+
# @return [Integer]
|
242
|
+
#
|
243
|
+
# @api private
|
244
|
+
def question_lines_count(question_lines)
|
245
|
+
question_lines.reduce(0) do |acc, line|
|
246
|
+
acc + @prompt.count_screen_lines(line)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
214
250
|
# Find value for the choice selected
|
215
251
|
#
|
216
252
|
# @return [nil, Object]
|
@@ -239,12 +275,12 @@ module TTY
|
|
239
275
|
#
|
240
276
|
# @api private
|
241
277
|
def render_question
|
242
|
-
header = "#{@prefix}#{@question} #{render_header}\n"
|
278
|
+
header = ["#{@prefix}#{@question} #{render_header}\n"]
|
243
279
|
unless @done
|
244
280
|
header << render_menu
|
245
281
|
header << render_footer
|
246
282
|
end
|
247
|
-
header
|
283
|
+
header.join
|
248
284
|
end
|
249
285
|
|
250
286
|
# Error message when incorrect index chosen
|
@@ -320,18 +356,23 @@ module TTY
|
|
320
356
|
#
|
321
357
|
# @api private
|
322
358
|
def render_menu
|
323
|
-
output =
|
359
|
+
output = []
|
360
|
+
|
324
361
|
@paginator.paginate(@choices, @page_active, @per_page) do |choice, index|
|
325
362
|
num = (index + 1).to_s + @enum + ' '
|
326
|
-
selected =
|
327
|
-
output << if index + 1 == @active
|
328
|
-
@prompt.decorate(selected
|
363
|
+
selected = num + choice.name
|
364
|
+
output << if index + 1 == @active && !choice.disabled?
|
365
|
+
(' ' * 2) + @prompt.decorate(selected, @active_color)
|
366
|
+
elsif choice.disabled?
|
367
|
+
@prompt.decorate(symbols[:cross], :red) + ' ' +
|
368
|
+
selected + ' ' + choice.disabled.to_s
|
329
369
|
else
|
330
|
-
selected
|
370
|
+
(' ' * 2) + selected
|
331
371
|
end
|
332
372
|
output << "\n"
|
333
373
|
end
|
334
|
-
|
374
|
+
|
375
|
+
output.join
|
335
376
|
end
|
336
377
|
end # EnumList
|
337
378
|
end # Prompt
|
data/lib/tty/prompt/evaluator.rb
CHANGED
data/lib/tty/prompt/expander.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require_relative 'choices'
|
4
5
|
|
@@ -13,7 +14,7 @@ module TTY
|
|
13
14
|
key: 'h',
|
14
15
|
name: 'print help',
|
15
16
|
value: :help
|
16
|
-
}
|
17
|
+
}.freeze
|
17
18
|
|
18
19
|
# Create instance of Expander
|
19
20
|
#
|
@@ -68,7 +69,7 @@ module TTY
|
|
68
69
|
@input = ''
|
69
70
|
end
|
70
71
|
end
|
71
|
-
|
72
|
+
alias keyreturn keyenter
|
72
73
|
|
73
74
|
# Respond to key press event
|
74
75
|
#
|
@@ -178,16 +179,16 @@ module TTY
|
|
178
179
|
#
|
179
180
|
# @api private
|
180
181
|
def render_header
|
181
|
-
header = "#{@prefix}#{@message} "
|
182
|
+
header = ["#{@prefix}#{@message} "]
|
182
183
|
if @done
|
183
|
-
selected_item =
|
184
|
+
selected_item = @selected.name.to_s
|
184
185
|
header << @prompt.decorate(selected_item, @active_color)
|
185
186
|
elsif collapsed?
|
186
187
|
header << %[(enter "h" for help) ]
|
187
188
|
header << "[#{possible_keys}] "
|
188
189
|
header << @input
|
189
190
|
end
|
190
|
-
header
|
191
|
+
header.join
|
191
192
|
end
|
192
193
|
|
193
194
|
# Show hint for selected option key
|
@@ -196,11 +197,10 @@ module TTY
|
|
196
197
|
#
|
197
198
|
# @api private
|
198
199
|
def render_hint
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
hint << @prompt.cursor.forward(@prompt.strip(render_header).size)
|
200
|
+
"\n" + @prompt.decorate('>> ', @active_color) +
|
201
|
+
@hint +
|
202
|
+
@prompt.cursor.prev_line +
|
203
|
+
@prompt.cursor.forward(@prompt.strip(render_header).size)
|
204
204
|
end
|
205
205
|
|
206
206
|
# Render question with menu
|
@@ -251,7 +251,7 @@ module TTY
|
|
251
251
|
#
|
252
252
|
# @api private
|
253
253
|
def render_menu
|
254
|
-
output = "\n"
|
254
|
+
output = ["\n"]
|
255
255
|
@choices.each do |choice|
|
256
256
|
chosen = %(#{choice.key} - #{choice.name})
|
257
257
|
if @selected && @selected.key == choice.key
|
@@ -259,7 +259,7 @@ module TTY
|
|
259
259
|
end
|
260
260
|
output << ' ' + chosen + "\n"
|
261
261
|
end
|
262
|
-
output
|
262
|
+
output.join
|
263
263
|
end
|
264
264
|
|
265
265
|
def setup_defaults
|
@@ -271,7 +271,7 @@ module TTY
|
|
271
271
|
keys = []
|
272
272
|
@choices.each do |choice|
|
273
273
|
if choice.key.nil?
|
274
|
-
errors <<
|
274
|
+
errors << "Choice #{choice.name} is missing a :key attribute"
|
275
275
|
next
|
276
276
|
end
|
277
277
|
if choice.key.length != 1
|
@@ -285,7 +285,7 @@ module TTY
|
|
285
285
|
end
|
286
286
|
keys << choice.key if choice.key
|
287
287
|
end
|
288
|
-
errors.each { |err|
|
288
|
+
errors.each { |err| raise ConfigurationError, err }
|
289
289
|
end
|
290
290
|
end # Expander
|
291
291
|
end # Prompt
|
data/lib/tty/prompt/keypress.rb
CHANGED
data/lib/tty/prompt/list.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
+
require 'English'
|
4
5
|
|
5
6
|
require_relative 'choices'
|
6
7
|
require_relative 'paginator'
|
@@ -15,9 +16,9 @@ module TTY
|
|
15
16
|
class List
|
16
17
|
include Symbols
|
17
18
|
|
18
|
-
HELP = '(Use arrow%s keys, press Enter to select%s)'
|
19
|
+
HELP = '(Use arrow%s keys, press Enter to select%s)'.freeze
|
19
20
|
|
20
|
-
PAGE_HELP = '(Move up or down to reveal more choices)'
|
21
|
+
PAGE_HELP = '(Move up or down to reveal more choices)'.freeze
|
21
22
|
|
22
23
|
# Allowed keys for filter, along with backspace and canc.
|
23
24
|
FILTER_KEYS_MATCHER = /\A\w\Z/
|
@@ -49,7 +50,7 @@ module TTY
|
|
49
50
|
@help_color = options.fetch(:help_color) { @prompt.help_color }
|
50
51
|
@marker = options.fetch(:marker) { symbols[:pointer] }
|
51
52
|
@cycle = options.fetch(:cycle) { false }
|
52
|
-
@filter = options.fetch(:filter) { false } ?
|
53
|
+
@filter = options.fetch(:filter) { false } ? '' : nil
|
53
54
|
@help = options[:help]
|
54
55
|
@first_render = true
|
55
56
|
@done = false
|
@@ -121,11 +122,11 @@ module TTY
|
|
121
122
|
def default_help
|
122
123
|
# Note that enumeration and filter are mutually exclusive
|
123
124
|
tokens = if enumerate?
|
124
|
-
[" or number (1-#{choices.size})",
|
125
|
+
[" or number (1-#{choices.size})", '']
|
125
126
|
elsif @filter
|
126
|
-
[
|
127
|
+
['', ", and letter keys to filter"]
|
127
128
|
else
|
128
|
-
[
|
129
|
+
['', '']
|
129
130
|
end
|
130
131
|
|
131
132
|
format(self.class::HELP, *tokens)
|
@@ -162,11 +163,12 @@ module TTY
|
|
162
163
|
@choices
|
163
164
|
else
|
164
165
|
@choices.select do |_choice|
|
165
|
-
_choice.
|
166
|
+
!_choice.disabled? &&
|
167
|
+
_choice.name.downcase.include?(@filter.downcase)
|
166
168
|
end
|
167
169
|
end
|
168
170
|
else
|
169
|
-
|
171
|
+
values.each { |val| @choices << val }
|
170
172
|
end
|
171
173
|
end
|
172
174
|
|
@@ -195,6 +197,7 @@ module TTY
|
|
195
197
|
return unless enumerate?
|
196
198
|
value = event.value.to_i
|
197
199
|
return unless (1..choices.count).cover?(value)
|
200
|
+
return if choices[value - 1].disabled?
|
198
201
|
@active = value
|
199
202
|
end
|
200
203
|
|
@@ -204,19 +207,35 @@ module TTY
|
|
204
207
|
alias keyreturn keyenter
|
205
208
|
alias keyspace keyenter
|
206
209
|
|
210
|
+
def search_choice_in(searchable)
|
211
|
+
searchable.find { |i| !choices[i - 1].disabled? }
|
212
|
+
end
|
213
|
+
|
207
214
|
def keyup(*)
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
215
|
+
searchable = (@active - 1).downto(1).to_a
|
216
|
+
prev_active = search_choice_in(searchable)
|
217
|
+
|
218
|
+
if prev_active
|
219
|
+
@active = prev_active
|
220
|
+
elsif @cycle
|
221
|
+
searchable = (choices.length).downto(1).to_a
|
222
|
+
prev_active = search_choice_in(searchable)
|
223
|
+
|
224
|
+
@active = prev_active if prev_active
|
212
225
|
end
|
213
226
|
end
|
214
227
|
|
215
228
|
def keydown(*)
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
229
|
+
searchable = ((@active + 1)..choices.length)
|
230
|
+
next_active = search_choice_in(searchable)
|
231
|
+
|
232
|
+
if next_active
|
233
|
+
@active = next_active
|
234
|
+
elsif @cycle
|
235
|
+
searchable = (1..choices.length)
|
236
|
+
next_active = search_choice_in(searchable)
|
237
|
+
|
238
|
+
@active = next_active if next_active
|
220
239
|
end
|
221
240
|
end
|
222
241
|
alias keytab keydown
|
@@ -233,7 +252,7 @@ module TTY
|
|
233
252
|
def keydelete(*)
|
234
253
|
return unless @filter
|
235
254
|
|
236
|
-
@filter =
|
255
|
+
@filter = ''
|
237
256
|
@active = 1
|
238
257
|
end
|
239
258
|
|
@@ -263,17 +282,22 @@ module TTY
|
|
263
282
|
|
264
283
|
# Validate default indexes to be within range
|
265
284
|
#
|
285
|
+
# @raise [ConfigurationError]
|
286
|
+
# raised when the default index is either non-integer,
|
287
|
+
# out of range or clashes with disabled choice item.
|
288
|
+
#
|
266
289
|
# @api private
|
267
290
|
def validate_defaults
|
268
291
|
@default.each do |d|
|
269
|
-
if d.nil? || d.to_s.empty?
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
292
|
+
msg = if d.nil? || d.to_s.empty?
|
293
|
+
"default index must be an integer in range (1 - #{choices.size})"
|
294
|
+
elsif d < 1 || d > choices.size
|
295
|
+
"default index `#{d}` out of range (1 - #{choices.size})"
|
296
|
+
elsif choices[d - 1] && choices[d - 1].disabled?
|
297
|
+
"default index `#{d}` matches disabled choice item"
|
298
|
+
end
|
299
|
+
|
300
|
+
raise(ConfigurationError, msg) if msg
|
277
301
|
end
|
278
302
|
end
|
279
303
|
|
@@ -296,7 +320,7 @@ module TTY
|
|
296
320
|
# matching lines), it won't be included by using String#lines.
|
297
321
|
question_lines = question.split($INPUT_RECORD_SEPARATOR, -1)
|
298
322
|
|
299
|
-
@prompt.print(refresh(question_lines
|
323
|
+
@prompt.print(refresh(question_lines_count(question_lines)))
|
300
324
|
end
|
301
325
|
@prompt.print(render_question)
|
302
326
|
answer
|
@@ -304,6 +328,17 @@ module TTY
|
|
304
328
|
@prompt.print(@prompt.show)
|
305
329
|
end
|
306
330
|
|
331
|
+
# Count how many screen lines the question spans
|
332
|
+
#
|
333
|
+
# @return [Integer]
|
334
|
+
#
|
335
|
+
# @api private
|
336
|
+
def question_lines_count(question_lines)
|
337
|
+
question_lines.reduce(0) do |acc, line|
|
338
|
+
acc + @prompt.count_screen_lines(line)
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
307
342
|
# Find value for the choice selected
|
308
343
|
#
|
309
344
|
# @return [nil, Object]
|
@@ -328,12 +363,13 @@ module TTY
|
|
328
363
|
#
|
329
364
|
# @api private
|
330
365
|
def render_question
|
331
|
-
header = "#{@prefix}#{@question} #{render_header}\n"
|
366
|
+
header = ["#{@prefix}#{@question} #{render_header}\n"]
|
332
367
|
@first_render = false
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
368
|
+
unless @done
|
369
|
+
header << render_menu
|
370
|
+
header << render_footer
|
371
|
+
end
|
372
|
+
header.join
|
337
373
|
end
|
338
374
|
|
339
375
|
# Header part showing the current filter
|
@@ -352,11 +388,11 @@ module TTY
|
|
352
388
|
# @api private
|
353
389
|
def render_header
|
354
390
|
if @done
|
355
|
-
selected_item =
|
391
|
+
selected_item = choices[@active - 1].name
|
356
392
|
@prompt.decorate(selected_item, @active_color)
|
357
393
|
elsif @first_render
|
358
394
|
@prompt.decorate(help, @help_color)
|
359
|
-
elsif @filter.to_s !=
|
395
|
+
elsif @filter.to_s != ''
|
360
396
|
@prompt.decorate(filter_help, @help_color)
|
361
397
|
end
|
362
398
|
end
|
@@ -367,13 +403,16 @@ module TTY
|
|
367
403
|
#
|
368
404
|
# @api private
|
369
405
|
def render_menu
|
370
|
-
output =
|
406
|
+
output = []
|
371
407
|
|
372
408
|
@paginator.paginate(choices, @active, @per_page) do |choice, index|
|
373
409
|
num = enumerate? ? (index + 1).to_s + @enum + ' ' : ''
|
374
|
-
message = if index + 1 == @active
|
410
|
+
message = if index + 1 == @active && !choice.disabled?
|
375
411
|
selected = @marker + ' ' + num + choice.name
|
376
|
-
@prompt.decorate(
|
412
|
+
@prompt.decorate(selected.to_s, @active_color)
|
413
|
+
elsif choice.disabled?
|
414
|
+
@prompt.decorate(symbols[:cross], :red) +
|
415
|
+
' ' + num + choice.name + ' ' + choice.disabled.to_s
|
377
416
|
else
|
378
417
|
' ' * 2 + num + choice.name
|
379
418
|
end
|
@@ -382,7 +421,7 @@ module TTY
|
|
382
421
|
output << (message + newline)
|
383
422
|
end
|
384
423
|
|
385
|
-
output
|
424
|
+
output.join
|
386
425
|
end
|
387
426
|
|
388
427
|
# Render page info footer
|
@@ -393,7 +432,7 @@ module TTY
|
|
393
432
|
def render_footer
|
394
433
|
return '' unless paginated?
|
395
434
|
colored_footer = @prompt.decorate(@page_help, @help_color)
|
396
|
-
"\n"
|
435
|
+
"\n" + colored_footer
|
397
436
|
end
|
398
437
|
end # List
|
399
438
|
end # Prompt
|