tty-prompt 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +1 -0
  3. data/CHANGELOG.md +17 -0
  4. data/Gemfile +2 -3
  5. data/README.md +140 -12
  6. data/appveyor.yml +1 -0
  7. data/examples/enum_select_disabled.rb +16 -0
  8. data/examples/{enum_paged.rb → enum_select_paged.rb} +0 -0
  9. data/examples/enum_select_wrapped.rb +15 -0
  10. data/examples/multi_select_disabled.rb +17 -0
  11. data/examples/multi_select_wrapped.rb +15 -0
  12. data/examples/select.rb +3 -1
  13. data/examples/select_disabled.rb +18 -0
  14. data/examples/{enum.rb → select_enum.rb} +0 -0
  15. data/examples/select_filtered.rb +3 -1
  16. data/examples/select_paginated.rb +3 -1
  17. data/examples/select_wrapped.rb +15 -0
  18. data/lib/tty/prompt.rb +1 -0
  19. data/lib/tty/prompt/answers_collector.rb +1 -0
  20. data/lib/tty/prompt/choice.rb +67 -25
  21. data/lib/tty/prompt/choices.rb +1 -0
  22. data/lib/tty/prompt/confirm_question.rb +3 -4
  23. data/lib/tty/prompt/converter_dsl.rb +1 -0
  24. data/lib/tty/prompt/converter_registry.rb +1 -0
  25. data/lib/tty/prompt/converters.rb +1 -0
  26. data/lib/tty/prompt/distance.rb +1 -0
  27. data/lib/tty/prompt/enum_list.rb +58 -17
  28. data/lib/tty/prompt/enum_paginator.rb +1 -0
  29. data/lib/tty/prompt/evaluator.rb +1 -0
  30. data/lib/tty/prompt/expander.rb +14 -14
  31. data/lib/tty/prompt/keypress.rb +1 -1
  32. data/lib/tty/prompt/list.rb +78 -39
  33. data/lib/tty/prompt/mask_question.rb +5 -4
  34. data/lib/tty/prompt/multi_list.rb +13 -3
  35. data/lib/tty/prompt/multiline.rb +6 -5
  36. data/lib/tty/prompt/paginator.rb +1 -0
  37. data/lib/tty/prompt/question.rb +10 -9
  38. data/lib/tty/prompt/question/checks.rb +1 -0
  39. data/lib/tty/prompt/question/modifier.rb +1 -0
  40. data/lib/tty/prompt/question/validation.rb +1 -0
  41. data/lib/tty/prompt/result.rb +1 -0
  42. data/lib/tty/prompt/slider.rb +3 -2
  43. data/lib/tty/prompt/statement.rb +1 -0
  44. data/lib/tty/prompt/suggestion.rb +4 -6
  45. data/lib/tty/prompt/symbols.rb +2 -1
  46. data/lib/tty/prompt/timeout.rb +16 -11
  47. data/lib/tty/prompt/utils.rb +1 -0
  48. data/lib/tty/prompt/version.rb +1 -1
  49. data/lib/tty/test_prompt.rb +1 -0
  50. metadata +11 -5
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'forwardable'
4
5
 
@@ -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
- result = ''
116
- result << "#{default ? positive.capitalize : positive.downcase}"
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
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative 'converter_registry'
4
5
 
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  module TTY
4
5
  class Prompt
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'pathname'
4
5
  require 'necromancer'
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  module TTY
4
5
  class Prompt
@@ -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
- PAGE_HELP = '(Press tab/right or left to reveal more choices)'.freeze
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
- values.each { |val| choice(*val) }
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
- if (@input.to_i > 0 && @input.to_i <= @choices.size) || @input.empty?
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
- if (@input.to_i > 0) && !@choices[@input.to_i - 1].nil?
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
- return if @default >= 1 && @default <= @choices.size
178
- raise ConfigurationError,
179
- "default index `#{@default}` out of range (1 - #{@choices.size})"
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
- @prompt.print(refresh(question.lines.count))
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 = ' ' * 2 + num + choice.name
327
- output << if index + 1 == @active
328
- @prompt.decorate(selected.to_s, @active_color)
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
- output
374
+
375
+ output.join
335
376
  end
336
377
  end # EnumList
337
378
  end # Prompt
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative 'paginator'
4
5
 
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require_relative 'result'
4
5
 
@@ -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
- alias_method :keyreturn, :keyenter
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 = "#{@selected.name}"
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
- hint = "\n"
200
- hint << @prompt.decorate('>> ', @active_color)
201
- hint << @hint
202
- hint << @prompt.cursor.prev_line
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 << "Choice #{choice.name} is missing a :key attribute"
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| fail ConfigurationError, err }
288
+ errors.each { |err| raise ConfigurationError, err }
289
289
  end
290
290
  end # Expander
291
291
  end # Prompt
@@ -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 'symbols'
@@ -95,7 +96,6 @@ module TTY
95
96
  else
96
97
  job.()
97
98
  end
98
- rescue Timeout::Error
99
99
  end
100
100
  end # Keypress
101
101
  end # Prompt
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
- require "English"
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 } ? "" : nil
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
- ["", ", and letter keys to filter"]
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.name.downcase.include?(@filter.downcase)
166
+ !_choice.disabled? &&
167
+ _choice.name.downcase.include?(@filter.downcase)
166
168
  end
167
169
  end
168
170
  else
169
- Array(values).each { |val| choice(*val) }
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
- if @active == 1
209
- @active = choices.length if @cycle
210
- else
211
- @active -= 1
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
- if @active == choices.length
217
- @active = 1 if @cycle
218
- else
219
- @active += 1
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
- raise ConfigurationError,
271
- "default index must be an integer in range (1 - #{choices.size})"
272
- end
273
- if d < 1 || d > choices.size
274
- raise ConfigurationError,
275
- "default index `#{d}` out of range (1 - #{choices.size})"
276
- end
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.count))
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
- rendered_menu = render_menu
334
- rendered_menu << render_footer
335
- header << rendered_menu unless @done
336
- header
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 = "#{choices[@active - 1].name}"
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("#{selected}", @active_color)
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" << colored_footer
435
+ "\n" + colored_footer
397
436
  end
398
437
  end # List
399
438
  end # Prompt