tty-prompt 0.18.0 → 0.22.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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -0
  3. data/README.md +549 -248
  4. data/lib/tty-prompt.rb +1 -2
  5. data/lib/tty/prompt.rb +187 -143
  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 +5 -7
  9. data/lib/tty/prompt/choices.rb +29 -11
  10. data/lib/tty/prompt/confirm_question.rb +38 -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 +57 -27
  16. data/lib/tty/prompt/errors.rb +31 -0
  17. data/lib/tty/prompt/evaluator.rb +1 -1
  18. data/lib/tty/prompt/expander.rb +39 -13
  19. data/lib/tty/prompt/keypress.rb +31 -36
  20. data/lib/tty/prompt/list.rb +175 -65
  21. data/lib/tty/prompt/mask_question.rb +4 -5
  22. data/lib/tty/prompt/multi_list.rb +124 -33
  23. data/lib/tty/prompt/multiline.rb +7 -6
  24. data/lib/tty/prompt/paginator.rb +38 -26
  25. data/lib/tty/prompt/question.rb +83 -34
  26. data/lib/tty/prompt/question/checks.rb +18 -0
  27. data/lib/tty/prompt/question/validation.rb +3 -3
  28. data/lib/tty/prompt/selected_choices.rb +76 -0
  29. data/lib/tty/prompt/slider.rb +83 -16
  30. data/lib/tty/prompt/statement.rb +3 -3
  31. data/lib/tty/prompt/suggestion.rb +6 -6
  32. data/lib/tty/prompt/symbols.rb +58 -34
  33. data/lib/tty/prompt/test.rb +36 -0
  34. data/lib/tty/prompt/timer.rb +75 -0
  35. data/lib/tty/prompt/utils.rb +1 -3
  36. data/lib/tty/prompt/version.rb +1 -1
  37. metadata +29 -227
  38. data/Rakefile +0 -8
  39. data/examples/ask.rb +0 -7
  40. data/examples/ask_valid.rb +0 -12
  41. data/examples/collect.rb +0 -21
  42. data/examples/echo.rb +0 -11
  43. data/examples/enum_select.rb +0 -7
  44. data/examples/enum_select_disabled.rb +0 -16
  45. data/examples/enum_select_paged.rb +0 -9
  46. data/examples/enum_select_wrapped.rb +0 -15
  47. data/examples/expand.rb +0 -29
  48. data/examples/in.rb +0 -9
  49. data/examples/inputs.rb +0 -10
  50. data/examples/key_events.rb +0 -15
  51. data/examples/keypress.rb +0 -9
  52. data/examples/mask.rb +0 -13
  53. data/examples/multi_select.rb +0 -8
  54. data/examples/multi_select_disabled.rb +0 -17
  55. data/examples/multi_select_paged.rb +0 -9
  56. data/examples/multi_select_wrapped.rb +0 -15
  57. data/examples/multiline.rb +0 -9
  58. data/examples/pause.rb +0 -9
  59. data/examples/select.rb +0 -20
  60. data/examples/select_disabled.rb +0 -18
  61. data/examples/select_enum.rb +0 -8
  62. data/examples/select_filtered.rb +0 -11
  63. data/examples/select_paginated.rb +0 -11
  64. data/examples/select_wrapped.rb +0 -15
  65. data/examples/slider.rb +0 -6
  66. data/examples/validation.rb +0 -9
  67. data/examples/yes_no.rb +0 -7
  68. data/lib/tty/prompt/messages.rb +0 -49
  69. data/lib/tty/prompt/timeout.rb +0 -78
  70. data/lib/tty/test_prompt.rb +0 -20
  71. data/spec/spec_helper.rb +0 -45
  72. data/spec/unit/ask_spec.rb +0 -132
  73. data/spec/unit/choice/eql_spec.rb +0 -22
  74. data/spec/unit/choice/from_spec.rb +0 -96
  75. data/spec/unit/choices/add_spec.rb +0 -12
  76. data/spec/unit/choices/each_spec.rb +0 -13
  77. data/spec/unit/choices/find_by_spec.rb +0 -10
  78. data/spec/unit/choices/new_spec.rb +0 -10
  79. data/spec/unit/choices/pluck_spec.rb +0 -9
  80. data/spec/unit/collect_spec.rb +0 -96
  81. data/spec/unit/converters/convert_bool_spec.rb +0 -58
  82. data/spec/unit/converters/convert_char_spec.rb +0 -11
  83. data/spec/unit/converters/convert_custom_spec.rb +0 -14
  84. data/spec/unit/converters/convert_date_spec.rb +0 -34
  85. data/spec/unit/converters/convert_file_spec.rb +0 -18
  86. data/spec/unit/converters/convert_number_spec.rb +0 -39
  87. data/spec/unit/converters/convert_path_spec.rb +0 -15
  88. data/spec/unit/converters/convert_range_spec.rb +0 -22
  89. data/spec/unit/converters/convert_regex_spec.rb +0 -12
  90. data/spec/unit/converters/convert_string_spec.rb +0 -21
  91. data/spec/unit/converters/on_error_spec.rb +0 -9
  92. data/spec/unit/distance/distance_spec.rb +0 -73
  93. data/spec/unit/enum_paginator_spec.rb +0 -75
  94. data/spec/unit/enum_select_spec.rb +0 -446
  95. data/spec/unit/error_spec.rb +0 -20
  96. data/spec/unit/evaluator_spec.rb +0 -67
  97. data/spec/unit/expand_spec.rb +0 -198
  98. data/spec/unit/keypress_spec.rb +0 -72
  99. data/spec/unit/mask_spec.rb +0 -132
  100. data/spec/unit/multi_select_spec.rb +0 -495
  101. data/spec/unit/multiline_spec.rb +0 -77
  102. data/spec/unit/new_spec.rb +0 -20
  103. data/spec/unit/ok_spec.rb +0 -10
  104. data/spec/unit/paginator_spec.rb +0 -73
  105. data/spec/unit/question/checks_spec.rb +0 -97
  106. data/spec/unit/question/default_spec.rb +0 -31
  107. data/spec/unit/question/echo_spec.rb +0 -38
  108. data/spec/unit/question/in_spec.rb +0 -115
  109. data/spec/unit/question/initialize_spec.rb +0 -12
  110. data/spec/unit/question/modifier/apply_to_spec.rb +0 -24
  111. data/spec/unit/question/modifier/letter_case_spec.rb +0 -41
  112. data/spec/unit/question/modifier/whitespace_spec.rb +0 -51
  113. data/spec/unit/question/modify_spec.rb +0 -41
  114. data/spec/unit/question/required_spec.rb +0 -92
  115. data/spec/unit/question/validate_spec.rb +0 -115
  116. data/spec/unit/question/validation/call_spec.rb +0 -31
  117. data/spec/unit/question/validation/coerce_spec.rb +0 -30
  118. data/spec/unit/result_spec.rb +0 -40
  119. data/spec/unit/say_spec.rb +0 -67
  120. data/spec/unit/select_spec.rb +0 -643
  121. data/spec/unit/slider_spec.rb +0 -100
  122. data/spec/unit/statement/initialize_spec.rb +0 -15
  123. data/spec/unit/subscribe_spec.rb +0 -22
  124. data/spec/unit/suggest_spec.rb +0 -28
  125. data/spec/unit/warn_spec.rb +0 -21
  126. data/spec/unit/yes_no_spec.rb +0 -251
  127. data/tasks/console.rake +0 -11
  128. data/tasks/coverage.rake +0 -11
  129. data/tasks/spec.rake +0 -29
  130. data/tty-prompt.gemspec +0 -33
@@ -1,74 +1,181 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pathname'
4
- require 'necromancer'
5
-
6
- require_relative 'converter_dsl'
3
+ require_relative "const"
4
+ require_relative "converter_dsl"
7
5
 
8
6
  module TTY
9
7
  class Prompt
10
8
  module Converters
11
9
  extend ConverterDSL
12
10
 
13
- # Delegate Necromancer errors
14
- #
15
- # @api private
16
- def self.on_error
17
- if block_given?
18
- yield
19
- else
20
- raise ArgumentError, 'You need to provide a block argument.'
11
+ TRUE_VALUES = /^(t(rue)?|y(es)?|on|1)$/i.freeze
12
+ FALSE_VALUES = /^(f(alse)?|n(o)?|off|0)$/i.freeze
13
+
14
+ SINGLE_DIGIT_MATCHER = /^(?<digit>\-?\d+(\.\d+)?)$/.freeze
15
+ DIGIT_MATCHER = /^(?<open>-?\d+(\.\d+)?)
16
+ \s*(?<sep>(\.\s*){2,3}|-|,)\s*
17
+ (?<close>-?\d+(\.\d+)?)$
18
+ /x.freeze
19
+ LETTER_MATCHER = /^(?<open>\w)
20
+ \s*(?<sep>(\.\s*){2,3}|-|,)\s*
21
+ (?<close>\w)$
22
+ /x.freeze
23
+
24
+ converter(:boolean, :bool) do |input|
25
+ case input.to_s
26
+ when TRUE_VALUES then true
27
+ when FALSE_VALUES then false
28
+ else Const::Undefined
21
29
  end
22
- rescue Necromancer::ConversionTypeError => e
23
- raise ConversionError, e.message
24
- end
25
-
26
- converter(:bool) do |input|
27
- on_error { Necromancer.convert(input).to(:boolean, strict: true) }
28
30
  end
29
31
 
30
- converter(:string) do |input|
32
+ converter(:string, :str) do |input|
31
33
  String(input).chomp
32
34
  end
33
35
 
34
- converter(:symbol) do |input|
36
+ converter(:symbol, :sym) do |input|
35
37
  input.to_sym
36
38
  end
37
39
 
40
+ converter(:char) do |input|
41
+ String(input).chars.to_a[0]
42
+ end
43
+
38
44
  converter(:date) do |input|
39
- on_error { Necromancer.convert(input).to(:date, strict: true) }
45
+ begin
46
+ require "date" unless defined?(::Date)
47
+ ::Date.parse(input)
48
+ rescue ArgumentError
49
+ Const::Undefined
50
+ end
40
51
  end
41
52
 
42
53
  converter(:datetime) do |input|
43
- on_error { Necromancer.convert(input).to(:datetime, strict: true) }
54
+ begin
55
+ require "date" unless defined?(::Date)
56
+ ::DateTime.parse(input.to_s)
57
+ rescue ArgumentError
58
+ Const::Undefined
59
+ end
44
60
  end
45
61
 
46
- converter(:int) do |input|
47
- on_error { Necromancer.convert(input).to(:integer, strict: true) }
62
+ converter(:time) do |input|
63
+ begin
64
+ require "time"
65
+ ::Time.parse(input.to_s)
66
+ rescue ArgumentError
67
+ Const::Undefined
68
+ end
69
+ end
70
+
71
+ converter(:integer, :int) do |input|
72
+ begin
73
+ Integer(input)
74
+ rescue ArgumentError
75
+ Const::Undefined
76
+ end
48
77
  end
49
78
 
50
79
  converter(:float) do |input|
51
- on_error { Necromancer.convert(input).to(:float, strict: true) }
80
+ begin
81
+ Float(input)
82
+ rescue TypeError, ArgumentError
83
+ Const::Undefined
84
+ end
52
85
  end
53
86
 
87
+ # Convert string number to integer or float
88
+ #
89
+ # @return [Integer,Float,Const::Undefined]
90
+ #
91
+ # @api private
92
+ def cast_to_num(num)
93
+ ([convert(:int, num), convert(:float, num)] - [Const::Undefined]).first ||
94
+ Const::Undefined
95
+ end
96
+ module_function :cast_to_num
97
+
54
98
  converter(:range) do |input|
55
- on_error { Necromancer.convert(input).to(:range, strict: true) }
99
+ if input.is_a?(::Range)
100
+ input
101
+ elsif match = input.to_s.match(SINGLE_DIGIT_MATCHER)
102
+ digit = cast_to_num(match[:digit])
103
+ ::Range.new(digit, digit)
104
+ elsif match = input.to_s.match(DIGIT_MATCHER)
105
+ open = cast_to_num(match[:open])
106
+ close = cast_to_num(match[:close])
107
+ ::Range.new(open, close, match[:sep].gsub(/\s*/, "") == "...")
108
+ elsif match = input.to_s.match(LETTER_MATCHER)
109
+ ::Range.new(match[:open], match[:close],
110
+ match[:sep].gsub(/\s*/, "") == "...")
111
+ else Const::Undefined
112
+ end
56
113
  end
57
114
 
58
115
  converter(:regexp) do |input|
59
116
  Regexp.new(input)
60
117
  end
61
118
 
62
- converter(:file) do |input|
63
- ::File.open(::File.join(Dir.pwd, input))
119
+ converter(:filepath, :file) do |input|
120
+ ::File.expand_path(input)
64
121
  end
65
122
 
66
- converter(:path) do |input|
67
- Pathname.new(::File.join(Dir.pwd, input))
123
+ converter(:pathname, :path) do |input|
124
+ require "pathname" unless defined?(::Pathname)
125
+ ::Pathname.new(input)
68
126
  end
69
127
 
70
- converter(:char) do |input|
71
- String(input).chars.to_a[0]
128
+ converter(:uri) do |input|
129
+ require "uri" unless defined?(::URI)
130
+ ::URI.parse(input)
131
+ end
132
+
133
+ converter(:list, :array) do |val|
134
+ (val.respond_to?(:to_a) ? val : val.split(/(?<!\\),/))
135
+ .map { |v| v.strip.gsub(/\\,/, ",") }
136
+ .reject(&:empty?)
137
+ end
138
+
139
+ converter(:hash, :map) do |val|
140
+ values = val.respond_to?(:to_a) ? val : val.split(/[& ]/)
141
+ values.each_with_object({}) do |pair, pairs|
142
+ key, value = pair.split(/[=:]/, 2)
143
+ if (current = pairs[key.to_sym])
144
+ pairs[key.to_sym] = Array(current) << value
145
+ else
146
+ pairs[key.to_sym] = value
147
+ end
148
+ pairs
149
+ end
150
+ end
151
+
152
+ converter_registry.keys.each do |type|
153
+ next if type =~ /list|array|map|hash/
154
+
155
+ [:"#{type}_list", :"#{type}_array", :"#{type}s"].each do |new_type|
156
+ converter(new_type) do |val|
157
+ converter_registry[:array].(val).map do |obj|
158
+ converter_registry[type].(obj)
159
+ end
160
+ end
161
+ end
162
+
163
+ [:"#{type}_map", :"#{type}_hash"].each do |new_type|
164
+ converter(new_type) do |val|
165
+ converter_registry[:hash].(val).each_with_object({}) do |(k, v), h|
166
+ h[k] = converter_registry[type].(v)
167
+ end
168
+ end
169
+ end
170
+
171
+ [:"string_#{type}_map", :"str_#{type}_map",
172
+ :"string_#{type}_hash", :"str_#{type}_hash"].each do |new_type|
173
+ converter(new_type) do |val|
174
+ converter_registry[:hash].(val).each_with_object({}) do |(k, v), h|
175
+ h[converter_registry[:string].(k)] = converter_registry[type].(v)
176
+ end
177
+ end
178
+ end
72
179
  end
73
180
  end # Converters
74
181
  end # Prompt
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'English'
3
+ require "English"
4
4
 
5
- require_relative 'choices'
6
- require_relative 'enum_paginator'
7
- require_relative 'paginator'
8
- require_relative 'symbols'
5
+ require_relative "choices"
6
+ require_relative "block_paginator"
7
+ require_relative "paginator"
9
8
 
10
9
  module TTY
11
10
  class Prompt
@@ -14,22 +13,22 @@ module TTY
14
13
  #
15
14
  # @api private
16
15
  class EnumList
17
- include Symbols
18
-
19
- PAGE_HELP = '(Press tab/right or left to reveal more choices)'
16
+ PAGE_HELP = "(Press tab/right or left to reveal more choices)"
20
17
 
21
18
  # Create instance of EnumList menu.
22
19
  #
23
20
  # @api public
24
- def initialize(prompt, options = {})
21
+ def initialize(prompt, **options)
25
22
  @prompt = prompt
26
23
  @prefix = options.fetch(:prefix) { @prompt.prefix }
27
- @enum = options.fetch(:enum) { ')' }
28
- @default = options.fetch(:default) { 1 }
24
+ @enum = options.fetch(:enum) { ")" }
25
+ @default = options.fetch(:default) { -1 }
29
26
  @active_color = options.fetch(:active_color) { @prompt.active_color }
30
27
  @help_color = options.fetch(:help_color) { @prompt.help_color }
31
28
  @error_color = options.fetch(:error_color) { @prompt.error_color }
32
29
  @cycle = options.fetch(:cycle) { false }
30
+ @quiet = options.fetch(:quiet) { @prompt.quiet }
31
+ @symbols = @prompt.symbols.merge(options.fetch(:symbols, {}))
33
32
  @input = nil
34
33
  @done = false
35
34
  @first_render = true
@@ -38,10 +37,21 @@ module TTY
38
37
  @choices = Choices.new
39
38
  @per_page = options[:per_page]
40
39
  @page_help = options[:page_help] || PAGE_HELP
41
- @paginator = EnumPaginator.new
40
+ @paginator = BlockPaginator.new
42
41
  @page_active = @default
43
42
  end
44
43
 
44
+ # Change symbols used by this prompt
45
+ #
46
+ # @param [Hash] new_symbols
47
+ # the new symbols to use
48
+ #
49
+ # @api public
50
+ def symbols(new_symbols = (not_set = true))
51
+ return @symbols if not_set
52
+ @symbols.merge!(new_symbols)
53
+ end
54
+
45
55
  # Set default option selected
46
56
  #
47
57
  # @api public
@@ -49,6 +59,15 @@ module TTY
49
59
  @default = default
50
60
  end
51
61
 
62
+ # Check if default value is set
63
+ #
64
+ # @return [Boolean]
65
+ #
66
+ # @api public
67
+ def default?
68
+ @default > 0
69
+ end
70
+
52
71
  # Set number of items per page
53
72
  #
54
73
  # @api public
@@ -83,6 +102,13 @@ module TTY
83
102
  @enum = value
84
103
  end
85
104
 
105
+ # Set quiet mode
106
+ #
107
+ # @api public
108
+ def quiet(value)
109
+ @quiet = value
110
+ end
111
+
86
112
  # Add a single choice
87
113
  #
88
114
  # @api public
@@ -125,7 +151,7 @@ module TTY
125
151
  end
126
152
 
127
153
  def keypress(event)
128
- if [:backspace, :delete].include?(event.key.name)
154
+ if %i[backspace delete].include?(event.key.name)
129
155
  return if @input.empty?
130
156
  @input.chop!
131
157
  mark_choice_as_active
@@ -144,7 +170,7 @@ module TTY
144
170
  if choice_in_range && !choice_disabled || @input.empty?
145
171
  @done = true
146
172
  else
147
- @input = ''
173
+ @input = ""
148
174
  @failure = true
149
175
  end
150
176
  end
@@ -169,6 +195,7 @@ module TTY
169
195
 
170
196
  private
171
197
 
198
+
172
199
  # Find active choice or set to default
173
200
  #
174
201
  # @return [nil]
@@ -206,6 +233,9 @@ module TTY
206
233
  #
207
234
  # @api private
208
235
  def setup_defaults
236
+ if !default?
237
+ @default = (0..choices.length).find {|i| !choices[i].disabled? } + 1
238
+ end
209
239
  validate_defaults
210
240
  mark_choice_as_active
211
241
  end
@@ -219,7 +249,7 @@ module TTY
219
249
  #
220
250
  # @api private
221
251
  def render
222
- @input = ''
252
+ @input = ""
223
253
  until @done
224
254
  question = render_question
225
255
  @prompt.print(question)
@@ -231,7 +261,7 @@ module TTY
231
261
  question_lines = question.split($INPUT_RECORD_SEPARATOR, -1)
232
262
  @prompt.print(refresh(question_lines_count(question_lines)))
233
263
  end
234
- @prompt.print(render_question)
264
+ @prompt.print(render_question) unless @quiet
235
265
  answer
236
266
  end
237
267
 
@@ -286,8 +316,8 @@ module TTY
286
316
  #
287
317
  # @api private
288
318
  def error_message
289
- error = 'Please enter a valid number'
290
- "\n" + @prompt.decorate('>>', @error_color) + ' ' + error
319
+ error = "Please enter a valid number"
320
+ "\n" + @prompt.decorate(">>", @error_color) + " " + error
291
321
  end
292
322
 
293
323
  # Render error message and return cursor to position of input
@@ -310,8 +340,8 @@ module TTY
310
340
  #
311
341
  # @api private
312
342
  def render_header
313
- return '' unless @done
314
- return '' unless @active
343
+ return "" unless @done
344
+ return "" unless @active
315
345
  selected_item = @choices[@active - 1].name.to_s
316
346
  @prompt.decorate(selected_item, @active_color)
317
347
  end
@@ -331,7 +361,7 @@ module TTY
331
361
  #
332
362
  # @api private
333
363
  def page_help_message
334
- return '' unless paginated?
364
+ return "" unless paginated?
335
365
  "\n" + @prompt.decorate(@page_help, @help_color)
336
366
  end
337
367
 
@@ -358,15 +388,15 @@ module TTY
358
388
  output = []
359
389
 
360
390
  @paginator.paginate(@choices, @page_active, @per_page) do |choice, index|
361
- num = (index + 1).to_s + @enum + ' '
362
- selected = num + choice.name
391
+ num = (index + 1).to_s + @enum + " "
392
+ selected = num.to_s + choice.name.to_s
363
393
  output << if index + 1 == @active && !choice.disabled?
364
- (' ' * 2) + @prompt.decorate(selected, @active_color)
394
+ (" " * 2) + @prompt.decorate(selected, @active_color)
365
395
  elsif choice.disabled?
366
- @prompt.decorate(symbols[:cross], :red) + ' ' +
367
- selected + ' ' + choice.disabled.to_s
396
+ @prompt.decorate(@symbols[:cross], :red) + " " +
397
+ selected + " " + choice.disabled.to_s
368
398
  else
369
- (' ' * 2) + selected
399
+ (" " * 2) + selected
370
400
  end
371
401
  output << "\n"
372
402
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Prompt
5
+ Error = Class.new(StandardError)
6
+
7
+ # Raised when wrong parameter is used to configure prompt
8
+ ConfigurationError = Class.new(Error)
9
+
10
+ # Raised when type conversion cannot be performed
11
+ ConversionError = Class.new(Error)
12
+
13
+ # Raised when the passed in validation argument is of wrong type
14
+ ValidationCoercion = Class.new(Error)
15
+
16
+ # Raised when the required argument is not supplied
17
+ ArgumentRequired = Class.new(Error)
18
+
19
+ # Raised when the argument validation fails
20
+ ArgumentValidation = Class.new(Error)
21
+
22
+ # Raised when the argument is not expected
23
+ InvalidArgument = Class.new(Error)
24
+
25
+ # Raised when overriding already defined conversion
26
+ ConversionAlreadyDefined = Class.new(Error)
27
+
28
+ # Raised when conversion type isn't registered
29
+ UnsupportedConversion = Class.new(Error)
30
+ end # Prompt
31
+ end # TTY
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'result'
3
+ require_relative "result"
4
4
 
5
5
  module TTY
6
6
  class Prompt