tty-prompt 0.19.0 → 0.23.1

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.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +485 -233
  5. data/lib/tty-prompt.rb +1 -2
  6. data/lib/tty/prompt.rb +159 -147
  7. data/lib/tty/prompt/answers_collector.rb +5 -5
  8. data/lib/tty/prompt/block_paginator.rb +1 -1
  9. data/lib/tty/prompt/choice.rb +31 -13
  10. data/lib/tty/prompt/choices.rb +30 -12
  11. data/lib/tty/prompt/confirm_question.rb +42 -16
  12. data/lib/tty/prompt/const.rb +17 -0
  13. data/lib/tty/prompt/converter_dsl.rb +6 -7
  14. data/lib/tty/prompt/converter_registry.rb +31 -26
  15. data/lib/tty/prompt/converters.rb +139 -32
  16. data/lib/tty/prompt/enum_list.rb +58 -25
  17. data/lib/tty/prompt/errors.rb +31 -0
  18. data/lib/tty/prompt/evaluator.rb +2 -2
  19. data/lib/tty/prompt/expander.rb +27 -15
  20. data/lib/tty/prompt/keypress.rb +5 -3
  21. data/lib/tty/prompt/list.rb +101 -38
  22. data/lib/tty/prompt/mask_question.rb +9 -5
  23. data/lib/tty/prompt/multi_list.rb +108 -29
  24. data/lib/tty/prompt/multiline.rb +9 -7
  25. data/lib/tty/prompt/paginator.rb +1 -1
  26. data/lib/tty/prompt/question.rb +67 -36
  27. data/lib/tty/prompt/question/checks.rb +20 -2
  28. data/lib/tty/prompt/question/modifier.rb +4 -2
  29. data/lib/tty/prompt/question/validation.rb +3 -3
  30. data/lib/tty/prompt/selected_choices.rb +77 -0
  31. data/lib/tty/prompt/slider.rb +110 -23
  32. data/lib/tty/prompt/statement.rb +3 -3
  33. data/lib/tty/prompt/suggestion.rb +7 -6
  34. data/lib/tty/prompt/symbols.rb +58 -58
  35. data/lib/tty/prompt/test.rb +36 -0
  36. data/lib/tty/prompt/utils.rb +1 -3
  37. data/lib/tty/prompt/version.rb +1 -1
  38. metadata +27 -196
  39. data/Rakefile +0 -8
  40. data/examples/ask.rb +0 -7
  41. data/examples/ask_blank.rb +0 -9
  42. data/examples/ask_valid.rb +0 -12
  43. data/examples/collect.rb +0 -21
  44. data/examples/echo.rb +0 -11
  45. data/examples/enum_select.rb +0 -7
  46. data/examples/enum_select_disabled.rb +0 -16
  47. data/examples/enum_select_paged.rb +0 -9
  48. data/examples/enum_select_wrapped.rb +0 -15
  49. data/examples/expand.rb +0 -29
  50. data/examples/expand_auto.rb +0 -29
  51. data/examples/in.rb +0 -9
  52. data/examples/inputs.rb +0 -10
  53. data/examples/key_events.rb +0 -15
  54. data/examples/keypress.rb +0 -9
  55. data/examples/mask.rb +0 -13
  56. data/examples/multi_select.rb +0 -8
  57. data/examples/multi_select_disabled.rb +0 -17
  58. data/examples/multi_select_disabled_paged.rb +0 -22
  59. data/examples/multi_select_paged.rb +0 -9
  60. data/examples/multi_select_wrapped.rb +0 -15
  61. data/examples/multiline.rb +0 -9
  62. data/examples/pause.rb +0 -9
  63. data/examples/select.rb +0 -24
  64. data/examples/select_disabled.rb +0 -18
  65. data/examples/select_disabled_paged.rb +0 -22
  66. data/examples/select_enum.rb +0 -8
  67. data/examples/select_filtered.rb +0 -11
  68. data/examples/select_paginated.rb +0 -11
  69. data/examples/select_wrapped.rb +0 -15
  70. data/examples/slider.rb +0 -6
  71. data/examples/validation.rb +0 -9
  72. data/examples/yes_no.rb +0 -7
  73. data/lib/tty/prompt/messages.rb +0 -49
  74. data/lib/tty/test_prompt.rb +0 -20
  75. data/spec/spec_helper.rb +0 -61
  76. data/spec/unit/ask_spec.rb +0 -173
  77. data/spec/unit/block_paginator_spec.rb +0 -84
  78. data/spec/unit/choice/eql_spec.rb +0 -22
  79. data/spec/unit/choice/from_spec.rb +0 -112
  80. data/spec/unit/choices/add_spec.rb +0 -12
  81. data/spec/unit/choices/each_spec.rb +0 -13
  82. data/spec/unit/choices/find_by_spec.rb +0 -10
  83. data/spec/unit/choices/new_spec.rb +0 -10
  84. data/spec/unit/choices/pluck_spec.rb +0 -9
  85. data/spec/unit/collect_spec.rb +0 -96
  86. data/spec/unit/converters/convert_bool_spec.rb +0 -58
  87. data/spec/unit/converters/convert_char_spec.rb +0 -11
  88. data/spec/unit/converters/convert_custom_spec.rb +0 -14
  89. data/spec/unit/converters/convert_date_spec.rb +0 -34
  90. data/spec/unit/converters/convert_file_spec.rb +0 -18
  91. data/spec/unit/converters/convert_number_spec.rb +0 -39
  92. data/spec/unit/converters/convert_path_spec.rb +0 -15
  93. data/spec/unit/converters/convert_range_spec.rb +0 -22
  94. data/spec/unit/converters/convert_regex_spec.rb +0 -12
  95. data/spec/unit/converters/convert_string_spec.rb +0 -21
  96. data/spec/unit/converters/on_error_spec.rb +0 -9
  97. data/spec/unit/distance/distance_spec.rb +0 -73
  98. data/spec/unit/enum_select_spec.rb +0 -518
  99. data/spec/unit/error_spec.rb +0 -20
  100. data/spec/unit/evaluator_spec.rb +0 -67
  101. data/spec/unit/expand_spec.rb +0 -290
  102. data/spec/unit/keypress_spec.rb +0 -66
  103. data/spec/unit/mask_spec.rb +0 -140
  104. data/spec/unit/multi_select_spec.rb +0 -741
  105. data/spec/unit/multiline_spec.rb +0 -77
  106. data/spec/unit/new_spec.rb +0 -20
  107. data/spec/unit/ok_spec.rb +0 -10
  108. data/spec/unit/paginator_spec.rb +0 -92
  109. data/spec/unit/question/checks_spec.rb +0 -97
  110. data/spec/unit/question/default_spec.rb +0 -31
  111. data/spec/unit/question/echo_spec.rb +0 -38
  112. data/spec/unit/question/in_spec.rb +0 -115
  113. data/spec/unit/question/initialize_spec.rb +0 -12
  114. data/spec/unit/question/modifier/apply_to_spec.rb +0 -24
  115. data/spec/unit/question/modifier/letter_case_spec.rb +0 -41
  116. data/spec/unit/question/modifier/whitespace_spec.rb +0 -51
  117. data/spec/unit/question/modify_spec.rb +0 -41
  118. data/spec/unit/question/required_spec.rb +0 -92
  119. data/spec/unit/question/validate_spec.rb +0 -115
  120. data/spec/unit/question/validation/call_spec.rb +0 -31
  121. data/spec/unit/question/validation/coerce_spec.rb +0 -30
  122. data/spec/unit/result_spec.rb +0 -40
  123. data/spec/unit/say_spec.rb +0 -67
  124. data/spec/unit/select_spec.rb +0 -942
  125. data/spec/unit/slider_spec.rb +0 -142
  126. data/spec/unit/statement/initialize_spec.rb +0 -15
  127. data/spec/unit/subscribe_spec.rb +0 -22
  128. data/spec/unit/suggest_spec.rb +0 -28
  129. data/spec/unit/timer_spec.rb +0 -29
  130. data/spec/unit/warn_spec.rb +0 -21
  131. data/spec/unit/yes_no_spec.rb +0 -251
  132. data/tasks/console.rake +0 -11
  133. data/tasks/coverage.rake +0 -11
  134. data/tasks/spec.rake +0 -29
  135. data/tty-prompt.gemspec +0 -31
@@ -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, value: value }
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
@@ -7,9 +7,9 @@ module TTY
7
7
  #
8
8
  # @api public
9
9
  class Slider
10
- HELP = '(Use arrow keys, press Enter to select)'.freeze
10
+ HELP = "(Use %s arrow keys, press Enter to select)"
11
11
 
12
- FORMAT = ':slider %d'.freeze
12
+ FORMAT = ":slider %s"
13
13
 
14
14
  # Initailize a Slider
15
15
  #
@@ -26,13 +26,17 @@ module TTY
26
26
  def initialize(prompt, **options)
27
27
  @prompt = prompt
28
28
  @prefix = options.fetch(:prefix) { @prompt.prefix }
29
- @min = options.fetch(:min) { 0 }
30
- @max = options.fetch(:max) { 10 }
31
- @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)
32
33
  @default = options[:default]
33
34
  @active_color = options.fetch(:active_color) { @prompt.active_color }
34
35
  @help_color = options.fetch(:help_color) { @prompt.help_color }
35
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 }
36
40
  @symbols = @prompt.symbols.merge(options.fetch(:symbols, {}))
37
41
  @first_render = true
38
42
  @done = false
@@ -46,6 +50,7 @@ module TTY
46
50
  # @api public
47
51
  def symbols(new_symbols = (not_set = true))
48
52
  return @symbols if not_set
53
+
49
54
  @symbols.merge!(new_symbols)
50
55
  end
51
56
 
@@ -56,19 +61,43 @@ module TTY
56
61
  # @api private
57
62
  def initial
58
63
  if @default.nil?
59
- 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)
60
69
  else
61
- range.index(@default)
70
+ # default is the index number
71
+ @default - 1
62
72
  end
63
73
  end
64
74
 
65
- # 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
66
84
  #
67
- # @return [Array[Integer]]
85
+ # @param [String] text
68
86
  #
69
- # @apip private
70
- def range
71
- (@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
72
101
  end
73
102
 
74
103
  # @api public
@@ -91,19 +120,57 @@ module TTY
91
120
  @step = value
92
121
  end
93
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
94
149
  def format(value)
95
150
  @format = value
96
151
  end
97
152
 
153
+ # Set quiet mode.
154
+ #
155
+ # @api public
156
+ def quiet(value)
157
+ @quiet = value
158
+ end
159
+
98
160
  # Call the slider by passing question
99
161
  #
100
162
  # @param [String] question
101
163
  # the question to ask
102
164
  #
103
165
  # @apu public
104
- def call(question, &block)
166
+ def call(question, possibilities = nil, &block)
105
167
  @question = question
168
+ choices(possibilities) if possibilities
106
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
+
107
174
  @active = initial
108
175
  @prompt.subscribe(self) do
109
176
  render
@@ -116,7 +183,7 @@ module TTY
116
183
  alias keydown keyleft
117
184
 
118
185
  def keyright(*)
119
- @active += 1 if (@active + 1) < range.size
186
+ @active += 1 if (@active + 1) < choices.size
120
187
  end
121
188
  alias keyup keyright
122
189
 
@@ -128,6 +195,20 @@ module TTY
128
195
 
129
196
  private
130
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
+
131
212
  # Render an interactive range slider.
132
213
  #
133
214
  # @api private
@@ -139,7 +220,7 @@ module TTY
139
220
  @prompt.read_keypress
140
221
  refresh(question.lines.count)
141
222
  end
142
- @prompt.print(render_question)
223
+ @prompt.print(render_question) unless @quiet
143
224
  answer
144
225
  ensure
145
226
  @prompt.print(@prompt.show)
@@ -155,11 +236,11 @@ module TTY
155
236
  @prompt.print(@prompt.clear_lines(lines))
156
237
  end
157
238
 
158
- # @return [Integer]
239
+ # @return [Integer, String]
159
240
  #
160
241
  # @api private
161
242
  def answer
162
- range[@active]
243
+ choices[@active].value
163
244
  end
164
245
 
165
246
  # Render question with the slider
@@ -170,13 +251,14 @@ module TTY
170
251
  def render_question
171
252
  header = ["#{@prefix}#{@question} "]
172
253
  if @done
173
- header << @prompt.decorate(answer.to_s, @active_color)
254
+ header << @prompt.decorate(choices[@active].to_s, @active_color)
174
255
  header << "\n"
175
256
  else
176
257
  header << render_slider
177
258
  end
178
- if @first_render
179
- 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)
180
262
  @first_render = false
181
263
  end
182
264
  header.join
@@ -190,9 +272,14 @@ module TTY
190
272
  def render_slider
191
273
  slider = (@symbols[:line] * @active) +
192
274
  @prompt.decorate(@symbols[:bullet], @active_color) +
193
- (@symbols[:line] * (range.size - @active - 1))
194
- value = " #{range[@active]}"
195
- @format.gsub(':slider', slider) % [value]
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
196
283
  end
197
284
  end # Slider
198
285
  end # Prompt
@@ -28,10 +28,10 @@ module TTY
28
28
  # change the message display to color
29
29
  #
30
30
  # @api public
31
- def initialize(prompt, options = {})
31
+ def initialize(prompt, newline: true, color: false)
32
32
  @prompt = prompt
33
- @newline = options.fetch(:newline) { true }
34
- @color = options.fetch(:color) { false }
33
+ @newline = newline
34
+ @color = color
35
35
  end
36
36
 
37
37
  # Output the message to the prompt
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'distance'
3
+ require_relative "distance"
4
4
 
5
5
  module TTY
6
6
  # A class responsible for terminal prompt interactions.
@@ -11,9 +11,9 @@ module TTY
11
11
  class Suggestion
12
12
  DEFAULT_INDENT = 8
13
13
 
14
- SINGLE_TEXT = 'Did you mean this?'
14
+ SINGLE_TEXT = "Did you mean this?"
15
15
 
16
- PLURAL_TEXT = 'Did you mean one of these?'
16
+ PLURAL_TEXT = "Did you mean one of these?"
17
17
 
18
18
  # Number of spaces
19
19
  #
@@ -33,7 +33,7 @@ module TTY
33
33
  # Initialize a Suggestion
34
34
  #
35
35
  # @api public
36
- def initialize(options = {})
36
+ def initialize(**options)
37
37
  @indent = options.fetch(:indent) { DEFAULT_INDENT }
38
38
  @single_text = options.fetch(:single_text) { SINGLE_TEXT }
39
39
  @plural_text = options.fetch(:plural_text) { PLURAL_TEXT }
@@ -88,6 +88,7 @@ module TTY
88
88
  # @api private
89
89
  def evaluate
90
90
  return @suggestions if @suggestions.empty?
91
+
91
92
  if @suggestions.one?
92
93
  build_single_suggestion
93
94
  else
@@ -97,14 +98,14 @@ module TTY
97
98
 
98
99
  # @api private
99
100
  def build_single_suggestion
100
- single_text + "\n" + (' ' * indent) + @suggestions.first
101
+ single_text + "\n" + (" " * indent) + @suggestions.first
101
102
  end
102
103
 
103
104
  # @api private
104
105
  def build_multiple_suggestions
105
106
  plural_text + "\n" +
106
107
  @suggestions.map do |sugest|
107
- ' ' * indent + sugest
108
+ " " * indent + sugest
108
109
  end.join("\n")
109
110
  end
110
111
  end # Suggestion