tty-prompt 0.18.1 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +95 -0
  3. data/README.md +598 -256
  4. data/lib/tty-prompt.rb +1 -2
  5. data/lib/tty/prompt.rb +192 -144
  6. data/lib/tty/prompt/answers_collector.rb +5 -5
  7. data/lib/tty/prompt/{enum_paginator.rb → block_paginator.rb} +20 -19
  8. data/lib/tty/prompt/choice.rb +9 -10
  9. data/lib/tty/prompt/choices.rb +30 -12
  10. data/lib/tty/prompt/confirm_question.rb +42 -16
  11. data/lib/tty/prompt/const.rb +17 -0
  12. data/lib/tty/prompt/converter_dsl.rb +6 -7
  13. data/lib/tty/prompt/converter_registry.rb +31 -26
  14. data/lib/tty/prompt/converters.rb +139 -32
  15. data/lib/tty/prompt/enum_list.rb +81 -26
  16. data/lib/tty/prompt/errors.rb +31 -0
  17. data/lib/tty/prompt/evaluator.rb +2 -2
  18. data/lib/tty/prompt/expander.rb +45 -15
  19. data/lib/tty/prompt/keypress.rb +33 -36
  20. data/lib/tty/prompt/list.rb +198 -63
  21. data/lib/tty/prompt/mask_question.rb +11 -8
  22. data/lib/tty/prompt/multi_list.rb +131 -28
  23. data/lib/tty/prompt/multiline.rb +9 -7
  24. data/lib/tty/prompt/paginator.rb +38 -26
  25. data/lib/tty/prompt/question.rb +92 -37
  26. data/lib/tty/prompt/question/checks.rb +20 -2
  27. data/lib/tty/prompt/question/modifier.rb +4 -2
  28. data/lib/tty/prompt/question/validation.rb +3 -3
  29. data/lib/tty/prompt/selected_choices.rb +77 -0
  30. data/lib/tty/prompt/slider.rb +125 -30
  31. data/lib/tty/prompt/statement.rb +3 -3
  32. data/lib/tty/prompt/suggestion.rb +7 -6
  33. data/lib/tty/prompt/symbols.rb +58 -34
  34. data/lib/tty/prompt/test.rb +36 -0
  35. data/lib/tty/prompt/timer.rb +75 -0
  36. data/lib/tty/prompt/utils.rb +1 -3
  37. data/lib/tty/prompt/version.rb +1 -1
  38. metadata +29 -227
  39. data/Rakefile +0 -8
  40. data/examples/ask.rb +0 -7
  41. data/examples/ask_valid.rb +0 -12
  42. data/examples/collect.rb +0 -21
  43. data/examples/echo.rb +0 -11
  44. data/examples/enum_select.rb +0 -7
  45. data/examples/enum_select_disabled.rb +0 -16
  46. data/examples/enum_select_paged.rb +0 -9
  47. data/examples/enum_select_wrapped.rb +0 -15
  48. data/examples/expand.rb +0 -29
  49. data/examples/in.rb +0 -9
  50. data/examples/inputs.rb +0 -10
  51. data/examples/key_events.rb +0 -15
  52. data/examples/keypress.rb +0 -9
  53. data/examples/mask.rb +0 -13
  54. data/examples/multi_select.rb +0 -8
  55. data/examples/multi_select_disabled.rb +0 -17
  56. data/examples/multi_select_paged.rb +0 -9
  57. data/examples/multi_select_wrapped.rb +0 -15
  58. data/examples/multiline.rb +0 -9
  59. data/examples/pause.rb +0 -9
  60. data/examples/select.rb +0 -24
  61. data/examples/select_disabled.rb +0 -18
  62. data/examples/select_enum.rb +0 -8
  63. data/examples/select_filtered.rb +0 -11
  64. data/examples/select_paginated.rb +0 -11
  65. data/examples/select_wrapped.rb +0 -15
  66. data/examples/slider.rb +0 -6
  67. data/examples/validation.rb +0 -9
  68. data/examples/yes_no.rb +0 -7
  69. data/lib/tty/prompt/messages.rb +0 -49
  70. data/lib/tty/prompt/timeout.rb +0 -78
  71. data/lib/tty/test_prompt.rb +0 -20
  72. data/spec/spec_helper.rb +0 -45
  73. data/spec/unit/ask_spec.rb +0 -132
  74. data/spec/unit/choice/eql_spec.rb +0 -22
  75. data/spec/unit/choice/from_spec.rb +0 -96
  76. data/spec/unit/choices/add_spec.rb +0 -12
  77. data/spec/unit/choices/each_spec.rb +0 -13
  78. data/spec/unit/choices/find_by_spec.rb +0 -10
  79. data/spec/unit/choices/new_spec.rb +0 -10
  80. data/spec/unit/choices/pluck_spec.rb +0 -9
  81. data/spec/unit/collect_spec.rb +0 -96
  82. data/spec/unit/converters/convert_bool_spec.rb +0 -58
  83. data/spec/unit/converters/convert_char_spec.rb +0 -11
  84. data/spec/unit/converters/convert_custom_spec.rb +0 -14
  85. data/spec/unit/converters/convert_date_spec.rb +0 -34
  86. data/spec/unit/converters/convert_file_spec.rb +0 -18
  87. data/spec/unit/converters/convert_number_spec.rb +0 -39
  88. data/spec/unit/converters/convert_path_spec.rb +0 -15
  89. data/spec/unit/converters/convert_range_spec.rb +0 -22
  90. data/spec/unit/converters/convert_regex_spec.rb +0 -12
  91. data/spec/unit/converters/convert_string_spec.rb +0 -21
  92. data/spec/unit/converters/on_error_spec.rb +0 -9
  93. data/spec/unit/distance/distance_spec.rb +0 -73
  94. data/spec/unit/enum_paginator_spec.rb +0 -75
  95. data/spec/unit/enum_select_spec.rb +0 -446
  96. data/spec/unit/error_spec.rb +0 -20
  97. data/spec/unit/evaluator_spec.rb +0 -67
  98. data/spec/unit/expand_spec.rb +0 -198
  99. data/spec/unit/keypress_spec.rb +0 -72
  100. data/spec/unit/mask_spec.rb +0 -132
  101. data/spec/unit/multi_select_spec.rb +0 -511
  102. data/spec/unit/multiline_spec.rb +0 -77
  103. data/spec/unit/new_spec.rb +0 -20
  104. data/spec/unit/ok_spec.rb +0 -10
  105. data/spec/unit/paginator_spec.rb +0 -73
  106. data/spec/unit/question/checks_spec.rb +0 -97
  107. data/spec/unit/question/default_spec.rb +0 -31
  108. data/spec/unit/question/echo_spec.rb +0 -38
  109. data/spec/unit/question/in_spec.rb +0 -115
  110. data/spec/unit/question/initialize_spec.rb +0 -12
  111. data/spec/unit/question/modifier/apply_to_spec.rb +0 -24
  112. data/spec/unit/question/modifier/letter_case_spec.rb +0 -41
  113. data/spec/unit/question/modifier/whitespace_spec.rb +0 -51
  114. data/spec/unit/question/modify_spec.rb +0 -41
  115. data/spec/unit/question/required_spec.rb +0 -92
  116. data/spec/unit/question/validate_spec.rb +0 -115
  117. data/spec/unit/question/validation/call_spec.rb +0 -31
  118. data/spec/unit/question/validation/coerce_spec.rb +0 -30
  119. data/spec/unit/result_spec.rb +0 -40
  120. data/spec/unit/say_spec.rb +0 -67
  121. data/spec/unit/select_spec.rb +0 -660
  122. data/spec/unit/slider_spec.rb +0 -100
  123. data/spec/unit/statement/initialize_spec.rb +0 -15
  124. data/spec/unit/subscribe_spec.rb +0 -22
  125. data/spec/unit/suggest_spec.rb +0 -28
  126. data/spec/unit/warn_spec.rb +0 -21
  127. data/spec/unit/yes_no_spec.rb +0 -251
  128. data/tasks/console.rake +0 -11
  129. data/tasks/coverage.rake +0 -11
  130. data/tasks/spec.rake +0 -29
  131. data/tty-prompt.gemspec +0 -33
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'converters'
4
- require_relative 'evaluator'
5
- require_relative 'question/modifier'
6
- require_relative 'question/validation'
7
- require_relative 'question/checks'
8
- require_relative 'utils'
3
+ require_relative "converters"
4
+ require_relative "evaluator"
5
+ require_relative "question/modifier"
6
+ require_relative "question/validation"
7
+ require_relative "question/checks"
8
+ require_relative "utils"
9
9
 
10
10
  module TTY
11
11
  # A class responsible for shell prompt interactions.
@@ -34,22 +34,31 @@ module TTY
34
34
  # Initialize a Question
35
35
  #
36
36
  # @api public
37
- def initialize(prompt, options = {})
38
- @prompt = prompt
39
- @prefix = options.fetch(:prefix) { @prompt.prefix }
40
- @default = options.fetch(:default) { UndefinedSetting }
41
- @required = options.fetch(:required) { false }
42
- @echo = options.fetch(:echo) { true }
43
- @in = options.fetch(:in) { UndefinedSetting }
44
- @modifier = options.fetch(:modifier) { [] }
45
- @validation = options.fetch(:validation) { UndefinedSetting }
46
- @convert = options.fetch(:convert) { UndefinedSetting }
37
+ def initialize(prompt, **options)
38
+ # Option deprecation
39
+ if options[:validation]
40
+ warn "[DEPRECATION] The `:validation` option is deprecated. Use `:validate` instead."
41
+ options[:validate] = options[:validation]
42
+ end
43
+
44
+ @prompt = prompt
45
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
46
+ @default = options.fetch(:default) { UndefinedSetting }
47
+ @required = options.fetch(:required) { false }
48
+ @echo = options.fetch(:echo) { true }
49
+ @in = options.fetch(:in) { UndefinedSetting }
50
+ @modifier = options.fetch(:modifier) { [] }
51
+ @validation = options.fetch(:validate) { UndefinedSetting }
52
+ @convert = options.fetch(:convert) { UndefinedSetting }
47
53
  @active_color = options.fetch(:active_color) { @prompt.active_color }
48
- @help_color = options.fetch(:help_color) { @prompt.help_color }
49
- @error_color = options.fetch(:error_color) { :red }
50
- @messages = Utils.deep_copy(options.fetch(:messages) { { } })
51
- @done = false
52
- @input = nil
54
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
55
+ @error_color = options.fetch(:error_color) { :red }
56
+ @value = options.fetch(:value) { UndefinedSetting }
57
+ @quiet = options.fetch(:quiet) { @prompt.quiet }
58
+ @messages = Utils.deep_copy(options.fetch(:messages) { {} })
59
+ @done = false
60
+ @first_render = true
61
+ @input = nil
53
62
 
54
63
  @evaluator = Evaluator.new(self)
55
64
 
@@ -58,6 +67,7 @@ module TTY
58
67
  @evaluator << CheckRange
59
68
  @evaluator << CheckValidation
60
69
  @evaluator << CheckModifier
70
+ @evaluator << CheckConversion
61
71
  end
62
72
 
63
73
  # Stores all the error messages displayed to user
@@ -83,7 +93,7 @@ module TTY
83
93
  if template && !template.match(/\%\{/).nil?
84
94
  [template % tokens]
85
95
  else
86
- [template || '']
96
+ [template || ""]
87
97
  end
88
98
  end
89
99
 
@@ -94,8 +104,7 @@ module TTY
94
104
  # @return [self]
95
105
  #
96
106
  # @api public
97
- def call(message, &block)
98
- return if Utils.blank?(message)
107
+ def call(message = "", &block)
99
108
  @message = message
100
109
  block.call(self) if block
101
110
  @prompt.subscribe(self) do
@@ -121,8 +130,8 @@ module TTY
121
130
  total_lines = @prompt.count_screen_lines(input_line)
122
131
  @prompt.print(refresh(question.lines.count, total_lines))
123
132
  end
124
- @prompt.print(render_question)
125
- convert_result(result.value)
133
+ @prompt.print(render_question) unless @quiet
134
+ result.value
126
135
  end
127
136
 
128
137
  # Render question
@@ -131,13 +140,16 @@ module TTY
131
140
  #
132
141
  # @api private
133
142
  def render_question
134
- header = ["#{@prefix}#{message} "]
143
+ header = []
144
+ if !Utils.blank?(@prefix) || !Utils.blank?(message)
145
+ header << "#{@prefix}#{message} "
146
+ end
135
147
  if !echo?
136
148
  header
137
149
  elsif @done
138
150
  header << @prompt.decorate(@input.to_s, @active_color)
139
151
  elsif default? && !Utils.blank?(@default)
140
- header << @prompt.decorate("(#{default})", @help_color) + ' '
152
+ header << @prompt.decorate("(#{default})", @help_color) + " "
141
153
  end
142
154
  header << "\n" if @done
143
155
  header.join
@@ -158,7 +170,12 @@ module TTY
158
170
  #
159
171
  # @api private
160
172
  def read_input(question)
161
- @prompt.read_line(question, echo: echo).chomp
173
+ options = { echo: echo }
174
+ if value? && @first_render
175
+ options[:value] = @value
176
+ @first_render = false
177
+ end
178
+ @prompt.read_line(question, **options).chomp
162
179
  end
163
180
 
164
181
  # Handle error condition
@@ -168,7 +185,7 @@ module TTY
168
185
  # @api private
169
186
  def render_error(errors)
170
187
  errors.reduce([]) do |acc, err|
171
- acc << @prompt.decorate('>>', :red) + ' ' + err
188
+ acc << @prompt.decorate(">>", :red) + " " + err
172
189
  acc
173
190
  end.join("\n")
174
191
  end
@@ -202,8 +219,13 @@ module TTY
202
219
  #
203
220
  # @api private
204
221
  def convert_result(value)
205
- if convert? & !Utils.blank?(value)
206
- Converters.convert(@convert, value)
222
+ if convert? && !Utils.blank?(value)
223
+ case @convert
224
+ when Proc
225
+ @convert.call(value)
226
+ else
227
+ Converters.convert(@convert, value)
228
+ end
207
229
  else
208
230
  value
209
231
  end
@@ -212,8 +234,13 @@ module TTY
212
234
  # Specify answer conversion
213
235
  #
214
236
  # @api public
215
- def convert(value)
216
- @convert = value
237
+ def convert(value = (not_set = true), message = nil)
238
+ messages[:convert?] = message if message
239
+ if not_set
240
+ @convert
241
+ else
242
+ @convert = value
243
+ end
217
244
  end
218
245
 
219
246
  # Check if conversion is set
@@ -230,6 +257,7 @@ module TTY
230
257
  # @api public
231
258
  def default(value = (not_set = true))
232
259
  return @default if not_set
260
+
233
261
  @default = value
234
262
  end
235
263
 
@@ -250,9 +278,10 @@ module TTY
250
278
  def required(value = (not_set = true), message = nil)
251
279
  messages[:required?] = message if message
252
280
  return @required if not_set
281
+
253
282
  @required = value
254
283
  end
255
- alias_method :required?, :required
284
+ alias required? required
256
285
 
257
286
  # Set validation rule for an argument
258
287
  #
@@ -266,6 +295,22 @@ module TTY
266
295
  @validation = (value || block)
267
296
  end
268
297
 
298
+ # Prepopulate input with custom content
299
+ #
300
+ # @api public
301
+ def value(val)
302
+ return @value if val.nil?
303
+
304
+ @value = val
305
+ end
306
+
307
+ # Check if custom value is present
308
+ #
309
+ # @api private
310
+ def value?
311
+ @value != UndefinedSetting
312
+ end
313
+
269
314
  def validation?
270
315
  @validation != UndefinedSetting
271
316
  end
@@ -285,18 +330,20 @@ module TTY
285
330
  # @api public
286
331
  def echo(value = nil)
287
332
  return @echo if value.nil?
333
+
288
334
  @echo = value
289
335
  end
290
- alias_method :echo?, :echo
336
+ alias echo? echo
291
337
 
292
338
  # Turn raw mode on or off. This enables character-based input.
293
339
  #
294
340
  # @api public
295
341
  def raw(value = nil)
296
342
  return @raw if value.nil?
343
+
297
344
  @raw = value
298
345
  end
299
- alias_method :raw?, :raw
346
+ alias raw? raw
300
347
 
301
348
  # Set expected range of values
302
349
  #
@@ -309,6 +356,7 @@ module TTY
309
356
  @in = Converters.convert(:range, @in)
310
357
  end
311
358
  return @in if not_set
359
+
312
360
  @in = Converters.convert(:range, value)
313
361
  end
314
362
 
@@ -321,6 +369,13 @@ module TTY
321
369
  @in != UndefinedSetting
322
370
  end
323
371
 
372
+ # Set quiet mode.
373
+ #
374
+ # @api public
375
+ def quiet(value)
376
+ @quiet = value
377
+ end
378
+
324
379
  # @api public
325
380
  def to_s
326
381
  message.to_s
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../const"
4
+
3
5
  module TTY
4
6
  class Prompt
5
7
  class Question
@@ -40,7 +42,7 @@ module TTY
40
42
  (question.in? && question.in.include?(cast(value)))
41
43
  [value]
42
44
  else
43
- tokens = {value: value, in: question.in}
45
+ tokens = { value: value, in: question.in }
44
46
  [value, question.message_for(:range?, tokens)]
45
47
  end
46
48
  end
@@ -54,7 +56,7 @@ module TTY
54
56
  Validation.new(question.validation).call(value))
55
57
  [value]
56
58
  else
57
- tokens = {valid: question.validation.inspect}
59
+ tokens = { valid: question.validation.inspect }
58
60
  [value, question.message_for(:valid?, tokens)]
59
61
  end
60
62
  end
@@ -81,6 +83,22 @@ module TTY
81
83
  end
82
84
  end
83
85
  end
86
+
87
+ class CheckConversion
88
+ def self.call(question, value)
89
+ if question.convert? && !Utils.blank?(value)
90
+ result = question.convert_result(value)
91
+ if result == Const::Undefined
92
+ tokens = { value: value, type: question.convert }
93
+ [value, question.message_for(:convert?, tokens)]
94
+ else
95
+ [result]
96
+ end
97
+ else
98
+ [value]
99
+ end
100
+ end
101
+ end
84
102
  end # Checks
85
103
  end # Question
86
104
  end # Prompt
@@ -49,6 +49,7 @@ module TTY
49
49
  # @api public
50
50
  def self.letter_case(mod, value)
51
51
  return value unless value.is_a?(String)
52
+
52
53
  case mod
53
54
  when :up, :upcase, :uppercase
54
55
  value.upcase
@@ -75,15 +76,16 @@ module TTY
75
76
  # @api public
76
77
  def self.whitespace(mod, value)
77
78
  return value unless value.is_a?(String)
79
+
78
80
  case mod
79
81
  when :trim, :strip
80
82
  value.strip
81
83
  when :chomp
82
84
  value.chomp
83
85
  when :collapse
84
- value.gsub(/\s+/, ' ')
86
+ value.gsub(/\s+/, " ")
85
87
  when :remove
86
- value.gsub(/\s+/, '')
88
+ value.gsub(/\s+/, "")
87
89
  else
88
90
  value
89
91
  end
@@ -57,11 +57,11 @@ module TTY
57
57
  def call(input)
58
58
  if pattern.is_a?(String) || pattern.is_a?(Symbol)
59
59
  VALIDATORS.key?(pattern.to_sym)
60
- !VALIDATORS[pattern.to_sym].match(input).nil?
60
+ !VALIDATORS[pattern.to_sym].match(input.to_s).nil?
61
61
  elsif pattern.is_a?(Regexp)
62
- !pattern.match(input).nil?
62
+ !pattern.match(input.to_s).nil?
63
63
  elsif pattern.is_a?(Proc)
64
- result = pattern.call(input)
64
+ result = pattern.call(input.to_s)
65
65
  result.nil? ? false : result
66
66
  else false
67
67
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Prompt
5
+ # @api private
6
+ class SelectedChoices
7
+ include Enumerable
8
+
9
+ attr_reader :size
10
+
11
+ # Create selected choices
12
+ #
13
+ # @param [Array<Choice>] selected
14
+ # @param [Array<Integer>] indexes
15
+ #
16
+ # @api public
17
+ def initialize(selected = [], indexes = [])
18
+ @selected = selected
19
+ @indexes = indexes
20
+ @size = @selected.size
21
+ end
22
+
23
+ # Clear selected choices
24
+ #
25
+ # @api public
26
+ def clear
27
+ @indexes.clear
28
+ @selected.clear
29
+ @size = 0
30
+ end
31
+
32
+ # Iterate over selected choices
33
+ #
34
+ # @api public
35
+ def each(&block)
36
+ return to_enum unless block_given?
37
+
38
+ @selected.each(&block)
39
+ end
40
+
41
+ # Insert choice at index
42
+ #
43
+ # @param [Integer] index
44
+ # @param [Choice] choice
45
+ #
46
+ # @api public
47
+ def insert(index, choice)
48
+ insert_idx = find_index_by { |i| index < @indexes[i] }
49
+ insert_idx ||= -1
50
+ @indexes.insert(insert_idx, index)
51
+ @selected.insert(insert_idx, choice)
52
+ @size += 1
53
+ self
54
+ end
55
+
56
+ # Delete choice at index
57
+ #
58
+ # @return [Choice]
59
+ # the deleted choice
60
+ #
61
+ # @api public
62
+ def delete_at(index)
63
+ delete_idx = @indexes.each_index.find { |i| index == @indexes[i] }
64
+ return nil unless delete_idx
65
+
66
+ @indexes.delete_at(delete_idx)
67
+ choice = @selected.delete_at(delete_idx)
68
+ @size -= 1
69
+ choice
70
+ end
71
+
72
+ def find_index_by(&search)
73
+ (0...@size).bsearch(&search)
74
+ end
75
+ end # SelectedChoices
76
+ end # Prompt
77
+ end # TTY
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'symbols'
4
-
5
3
  module TTY
6
4
  # A class responsible for shell prompt interactions.
7
5
  class Prompt
@@ -9,11 +7,9 @@ module TTY
9
7
  #
10
8
  # @api public
11
9
  class Slider
12
- include Symbols
13
-
14
- HELP = '(Use arrow keys, press Enter to select)'.freeze
10
+ HELP = "(Use %s arrow keys, press Enter to select)"
15
11
 
16
- FORMAT = ':slider %d'.freeze
12
+ FORMAT = ":slider %s"
17
13
 
18
14
  # Initailize a Slider
19
15
  #
@@ -27,20 +23,37 @@ module TTY
27
23
  # @option options [String] :format The display format
28
24
  #
29
25
  # @api public
30
- def initialize(prompt, options = {})
26
+ def initialize(prompt, **options)
31
27
  @prompt = prompt
32
28
  @prefix = options.fetch(:prefix) { @prompt.prefix }
33
- @min = options.fetch(:min) { 0 }
34
- @max = options.fetch(:max) { 10 }
35
- @step = options.fetch(:step) { 1 }
29
+ @choices = Choices.new
30
+ @min = options.fetch(:min, 0)
31
+ @max = options.fetch(:max, 10)
32
+ @step = options.fetch(:step, 1)
36
33
  @default = options[:default]
37
34
  @active_color = options.fetch(:active_color) { @prompt.active_color }
38
35
  @help_color = options.fetch(:help_color) { @prompt.help_color }
39
36
  @format = options.fetch(:format) { FORMAT }
37
+ @quiet = options.fetch(:quiet) { @prompt.quiet }
38
+ @help = options[:help]
39
+ @show_help = options.fetch(:show_help) { :start }
40
+ @symbols = @prompt.symbols.merge(options.fetch(:symbols, {}))
40
41
  @first_render = true
41
42
  @done = false
42
43
  end
43
44
 
45
+ # Change symbols used by this prompt
46
+ #
47
+ # @param [Hash] new_symbols
48
+ # the new symbols to use
49
+ #
50
+ # @api public
51
+ def symbols(new_symbols = (not_set = true))
52
+ return @symbols if not_set
53
+
54
+ @symbols.merge!(new_symbols)
55
+ end
56
+
44
57
  # Setup initial active position
45
58
  #
46
59
  # @return [Integer]
@@ -48,19 +61,43 @@ module TTY
48
61
  # @api private
49
62
  def initial
50
63
  if @default.nil?
51
- range.size / 2
64
+ # no default - choose the middle option
65
+ choices.size / 2
66
+ elsif default_choice = choices.find_by(:name, @default)
67
+ # found a Choice by name - use it
68
+ choices.index(default_choice)
52
69
  else
53
- range.index(@default)
70
+ # default is the index number
71
+ @default - 1
54
72
  end
55
73
  end
56
74
 
57
- # Range of numbers to render
75
+ # Default help text
76
+ #
77
+ # @api public
78
+ def default_help
79
+ arrows = @symbols[:arrow_left] + "/" + @symbols[:arrow_right]
80
+ sprintf(HELP, arrows)
81
+ end
82
+
83
+ # Set help text
58
84
  #
59
- # @return [Array[Integer]]
85
+ # @param [String] text
60
86
  #
61
- # @apip private
62
- def range
63
- (@min..@max).step(@step).to_a
87
+ # @api private
88
+ def help(text = (not_set = true))
89
+ return @help if !@help.nil? && not_set
90
+
91
+ @help = (@help.nil? && not_set) ? default_help : text
92
+ end
93
+
94
+ # Change when help is displayed
95
+ #
96
+ # @api public
97
+ def show_help(value = (not_set = true))
98
+ return @show_ehlp if not_set
99
+
100
+ @show_help = value
64
101
  end
65
102
 
66
103
  # @api public
@@ -83,19 +120,57 @@ module TTY
83
120
  @step = value
84
121
  end
85
122
 
123
+ # Add a single choice
124
+ #
125
+ # @api public
126
+ def choice(*value, &block)
127
+ if block
128
+ @choices << (value << block)
129
+ else
130
+ @choices << value
131
+ end
132
+ end
133
+
134
+ # Add multiple choices
135
+ #
136
+ # @param [Array[Object]] values
137
+ # the values to add as choices
138
+ #
139
+ # @api public
140
+ def choices(values = (not_set = true))
141
+ if not_set
142
+ @choices
143
+ else
144
+ values.each { |val| @choices << val }
145
+ end
146
+ end
147
+
148
+ # @api public
86
149
  def format(value)
87
150
  @format = value
88
151
  end
89
152
 
153
+ # Set quiet mode.
154
+ #
155
+ # @api public
156
+ def quiet(value)
157
+ @quiet = value
158
+ end
159
+
90
160
  # Call the slider by passing question
91
161
  #
92
162
  # @param [String] question
93
163
  # the question to ask
94
164
  #
95
165
  # @apu public
96
- def call(question, &block)
166
+ def call(question, possibilities = nil, &block)
97
167
  @question = question
168
+ choices(possibilities) if possibilities
98
169
  block.call(self) if block
170
+ # set up a Choices collection for min, max, step
171
+ # if no possibilities were supplied
172
+ choices((@min..@max).step(@step).to_a) if @choices.empty?
173
+
99
174
  @active = initial
100
175
  @prompt.subscribe(self) do
101
176
  render
@@ -108,7 +183,7 @@ module TTY
108
183
  alias keydown keyleft
109
184
 
110
185
  def keyright(*)
111
- @active += 1 if (@active + 1) < range.size
186
+ @active += 1 if (@active + 1) < choices.size
112
187
  end
113
188
  alias keyup keyright
114
189
 
@@ -120,6 +195,20 @@ module TTY
120
195
 
121
196
  private
122
197
 
198
+ # Check if help is shown only on start
199
+ #
200
+ # @api private
201
+ def help_start?
202
+ @show_help =~ /start/i
203
+ end
204
+
205
+ # Check if help is always displayed
206
+ #
207
+ # @api private
208
+ def help_always?
209
+ @show_help =~ /always/i
210
+ end
211
+
123
212
  # Render an interactive range slider.
124
213
  #
125
214
  # @api private
@@ -131,7 +220,7 @@ module TTY
131
220
  @prompt.read_keypress
132
221
  refresh(question.lines.count)
133
222
  end
134
- @prompt.print(render_question)
223
+ @prompt.print(render_question) unless @quiet
135
224
  answer
136
225
  ensure
137
226
  @prompt.print(@prompt.show)
@@ -147,11 +236,11 @@ module TTY
147
236
  @prompt.print(@prompt.clear_lines(lines))
148
237
  end
149
238
 
150
- # @return [Integer]
239
+ # @return [Integer, String]
151
240
  #
152
241
  # @api private
153
242
  def answer
154
- range[@active]
243
+ choices[@active].value
155
244
  end
156
245
 
157
246
  # Render question with the slider
@@ -162,13 +251,14 @@ module TTY
162
251
  def render_question
163
252
  header = ["#{@prefix}#{@question} "]
164
253
  if @done
165
- header << @prompt.decorate(answer.to_s, @active_color)
254
+ header << @prompt.decorate(choices[@active].to_s, @active_color)
166
255
  header << "\n"
167
256
  else
168
257
  header << render_slider
169
258
  end
170
- if @first_render
171
- header << "\n" + @prompt.decorate(HELP, @help_color)
259
+ if @first_render && (help_start? || help_always?) ||
260
+ (help_always? && !@done)
261
+ header << "\n" + @prompt.decorate(help, @help_color)
172
262
  @first_render = false
173
263
  end
174
264
  header.join
@@ -180,11 +270,16 @@ module TTY
180
270
  #
181
271
  # @api private
182
272
  def render_slider
183
- slider = (symbols[:line] * @active) +
184
- @prompt.decorate(symbols[:handle], @active_color) +
185
- (symbols[:line] * (range.size - @active - 1))
186
- value = " #{range[@active]}"
187
- @format.gsub(':slider', slider) % [value]
273
+ slider = (@symbols[:line] * @active) +
274
+ @prompt.decorate(@symbols[:bullet], @active_color) +
275
+ (@symbols[:line] * (choices.size - @active - 1))
276
+ value = choices[@active].name
277
+ case @format
278
+ when Proc
279
+ @format.call(slider, value)
280
+ else
281
+ @format.gsub(":slider", slider) % [value]
282
+ end
188
283
  end
189
284
  end # Slider
190
285
  end # Prompt