tty2-prompt 0.23.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/LICENSE.txt +23 -0
  4. data/README.md +52 -0
  5. data/lib/tty2/prompt/answers_collector.rb +78 -0
  6. data/lib/tty2/prompt/block_paginator.rb +59 -0
  7. data/lib/tty2/prompt/choice.rb +147 -0
  8. data/lib/tty2/prompt/choices.rb +129 -0
  9. data/lib/tty2/prompt/confirm_question.rb +158 -0
  10. data/lib/tty2/prompt/const.rb +17 -0
  11. data/lib/tty2/prompt/converter_dsl.rb +21 -0
  12. data/lib/tty2/prompt/converter_registry.rb +69 -0
  13. data/lib/tty2/prompt/converters.rb +182 -0
  14. data/lib/tty2/prompt/distance.rb +49 -0
  15. data/lib/tty2/prompt/enum_list.rb +433 -0
  16. data/lib/tty2/prompt/errors.rb +31 -0
  17. data/lib/tty2/prompt/evaluator.rb +29 -0
  18. data/lib/tty2/prompt/expander.rb +321 -0
  19. data/lib/tty2/prompt/keypress.rb +98 -0
  20. data/lib/tty2/prompt/list.rb +589 -0
  21. data/lib/tty2/prompt/mask_question.rb +96 -0
  22. data/lib/tty2/prompt/multi_list.rb +224 -0
  23. data/lib/tty2/prompt/multiline.rb +72 -0
  24. data/lib/tty2/prompt/paginator.rb +111 -0
  25. data/lib/tty2/prompt/question/checks.rb +105 -0
  26. data/lib/tty2/prompt/question/modifier.rb +96 -0
  27. data/lib/tty2/prompt/question/validation.rb +72 -0
  28. data/lib/tty2/prompt/question.rb +391 -0
  29. data/lib/tty2/prompt/result.rb +42 -0
  30. data/lib/tty2/prompt/selected_choices.rb +77 -0
  31. data/lib/tty2/prompt/slider.rb +286 -0
  32. data/lib/tty2/prompt/statement.rb +55 -0
  33. data/lib/tty2/prompt/suggestion.rb +113 -0
  34. data/lib/tty2/prompt/symbols.rb +89 -0
  35. data/lib/tty2/prompt/test.rb +36 -0
  36. data/lib/tty2/prompt/timer.rb +75 -0
  37. data/lib/tty2/prompt/utils.rb +42 -0
  38. data/lib/tty2/prompt/version.rb +7 -0
  39. data/lib/tty2/prompt.rb +589 -0
  40. data/lib/tty2-prompt.rb +1 -0
  41. metadata +148 -0
@@ -0,0 +1,391 @@
1
+ # frozen_string_literal: true
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"
9
+
10
+ module TTY2
11
+ # A class responsible for shell prompt interactions.
12
+ class Prompt
13
+ # A class responsible for gathering user input
14
+ #
15
+ # @api public
16
+ class Question
17
+ include Checks
18
+
19
+ UndefinedSetting = Class.new do
20
+ def to_s
21
+ "undefined"
22
+ end
23
+ alias_method :inspect, :to_s
24
+ end
25
+
26
+ # Store question message
27
+ # @api public
28
+ attr_reader :message
29
+
30
+ attr_reader :modifier
31
+
32
+ attr_reader :validation
33
+
34
+ # Initialize a Question
35
+ #
36
+ # @api public
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 }
53
+ @active_color = options.fetch(:active_color) { @prompt.active_color }
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
62
+
63
+ @evaluator = Evaluator.new(self)
64
+
65
+ @evaluator << CheckRequired
66
+ @evaluator << CheckDefault
67
+ @evaluator << CheckRange
68
+ @evaluator << CheckValidation
69
+ @evaluator << CheckModifier
70
+ @evaluator << CheckConversion
71
+ end
72
+
73
+ # Stores all the error messages displayed to user
74
+ # The currently supported messages are:
75
+ # * :range?
76
+ # * :required?
77
+ # * :valid?
78
+ attr_reader :messages
79
+
80
+ # Retrieve message based on the key
81
+ #
82
+ # @param [Symbol] name
83
+ # the name of message key
84
+ #
85
+ # @param [Hash] tokens
86
+ # the tokens to evaluate
87
+ #
88
+ # @return [Array[String]]
89
+ #
90
+ # @api private
91
+ def message_for(name, tokens = nil)
92
+ template = @messages[name]
93
+ if template && !template.match(/\%\{/).nil?
94
+ [template % tokens]
95
+ else
96
+ [template || ""]
97
+ end
98
+ end
99
+
100
+ # Call the question
101
+ #
102
+ # @param [String] message
103
+ #
104
+ # @return [self]
105
+ #
106
+ # @api public
107
+ def call(message = "", &block)
108
+ @message = message
109
+ block.call(self) if block
110
+ @prompt.subscribe(self) do
111
+ render
112
+ end
113
+ end
114
+
115
+ # Read answer and convert to type
116
+ #
117
+ # @api private
118
+ def render
119
+ @errors = []
120
+ until @done
121
+ result = process_input(render_question)
122
+ if result.failure?
123
+ @errors = result.errors
124
+ @prompt.print(render_error(result.errors))
125
+ else
126
+ @done = true
127
+ end
128
+ question = render_question
129
+ input_line = question + result.value.to_s
130
+ total_lines = @prompt.count_screen_lines(input_line)
131
+ @prompt.print(refresh(question.lines.count, total_lines))
132
+ end
133
+ @prompt.print(render_question) unless @quiet
134
+ result.value
135
+ end
136
+
137
+ # Render question
138
+ #
139
+ # @return [String]
140
+ #
141
+ # @api private
142
+ def render_question
143
+ header = []
144
+ if !Utils.blank?(@prefix) || !Utils.blank?(message)
145
+ header << "#{@prefix}#{message} "
146
+ end
147
+ if !echo?
148
+ header
149
+ elsif @done
150
+ header << @prompt.decorate(@input.to_s, @active_color)
151
+ elsif default? && !Utils.blank?(@default)
152
+ header << @prompt.decorate("(#{default})", @help_color) + " "
153
+ end
154
+ header << "\n" if @done
155
+ header.join
156
+ end
157
+
158
+ # Decide how to handle input from user
159
+ #
160
+ # @api private
161
+ def process_input(question)
162
+ @input = read_input(question)
163
+ if Utils.blank?(@input)
164
+ @input = default? ? default : nil
165
+ end
166
+ @evaluator.(@input)
167
+ end
168
+
169
+ # Process input
170
+ #
171
+ # @api private
172
+ def read_input(question)
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
179
+ end
180
+
181
+ # Handle error condition
182
+ #
183
+ # @return [String]
184
+ #
185
+ # @api private
186
+ def render_error(errors)
187
+ errors.reduce([]) do |acc, err|
188
+ acc << @prompt.decorate(">>", :red) + " " + err
189
+ acc
190
+ end.join("\n")
191
+ end
192
+
193
+ # Determine area of the screen to clear
194
+ #
195
+ # @param [Integer] lines
196
+ # number of lines to clear
197
+ #
198
+ # @return [String]
199
+ #
200
+ # @api private
201
+ def refresh(lines, lines_to_clear)
202
+ output = []
203
+ if @done
204
+ if @errors.count.zero?
205
+ output << @prompt.cursor.up(lines)
206
+ else
207
+ lines += @errors.count
208
+ lines_to_clear += @errors.count
209
+ end
210
+ else
211
+ output << @prompt.cursor.up(lines)
212
+ end
213
+ output.join + @prompt.clear_lines(lines_to_clear)
214
+ end
215
+
216
+ # Convert value to expected type
217
+ #
218
+ # @param [Object] value
219
+ #
220
+ # @api private
221
+ def convert_result(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
229
+ else
230
+ value
231
+ end
232
+ end
233
+
234
+ # Specify answer conversion
235
+ #
236
+ # @api public
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
244
+ end
245
+
246
+ # Check if conversion is set
247
+ #
248
+ # @return [Boolean]
249
+ #
250
+ # @api public
251
+ def convert?
252
+ @convert != UndefinedSetting
253
+ end
254
+
255
+ # Set default value.
256
+ #
257
+ # @api public
258
+ def default(value = (not_set = true))
259
+ return @default if not_set
260
+
261
+ @default = value
262
+ end
263
+
264
+ # Check if default value is set
265
+ #
266
+ # @return [Boolean]
267
+ #
268
+ # @api public
269
+ def default?
270
+ @default != UndefinedSetting
271
+ end
272
+
273
+ # Ensure that passed argument is present or not
274
+ #
275
+ # @return [Boolean]
276
+ #
277
+ # @api public
278
+ def required(value = (not_set = true), message = nil)
279
+ messages[:required?] = message if message
280
+ return @required if not_set
281
+
282
+ @required = value
283
+ end
284
+ alias required? required
285
+
286
+ # Set validation rule for an argument
287
+ #
288
+ # @param [Object] value
289
+ #
290
+ # @return [Question]
291
+ #
292
+ # @api public
293
+ def validate(value = nil, message = nil, &block)
294
+ messages[:valid?] = message if message
295
+ @validation = (value || block)
296
+ end
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
+
314
+ def validation?
315
+ @validation != UndefinedSetting
316
+ end
317
+
318
+ # Modify string according to the rule given.
319
+ #
320
+ # @param [Symbol] rule
321
+ #
322
+ # @api public
323
+ def modify(*rules)
324
+ @modifier = rules
325
+ end
326
+
327
+ # Turn terminal echo on or off. This is used to secure the display so
328
+ # that the entered characters are not echoed back to the screen.
329
+ #
330
+ # @api public
331
+ def echo(value = nil)
332
+ return @echo if value.nil?
333
+
334
+ @echo = value
335
+ end
336
+ alias echo? echo
337
+
338
+ # Turn raw mode on or off. This enables character-based input.
339
+ #
340
+ # @api public
341
+ def raw(value = nil)
342
+ return @raw if value.nil?
343
+
344
+ @raw = value
345
+ end
346
+ alias raw? raw
347
+
348
+ # Set expected range of values
349
+ #
350
+ # @param [String] value
351
+ #
352
+ # @api public
353
+ def in(value = (not_set = true), message = nil)
354
+ messages[:range?] = message if message
355
+ if in? && !@in.is_a?(Range)
356
+ @in = Converters.convert(:range, @in)
357
+ end
358
+ return @in if not_set
359
+
360
+ @in = Converters.convert(:range, value)
361
+ end
362
+
363
+ # Check if range is set
364
+ #
365
+ # @return [Boolean]
366
+ #
367
+ # @api public
368
+ def in?
369
+ @in != UndefinedSetting
370
+ end
371
+
372
+ # Set quiet mode.
373
+ #
374
+ # @api public
375
+ def quiet(value)
376
+ @quiet = value
377
+ end
378
+
379
+ # @api public
380
+ def to_s
381
+ message.to_s
382
+ end
383
+
384
+ # String representation of this question
385
+ # @api public
386
+ def inspect
387
+ "#<#{self.class.name} @message=#{message}, @input=#{@input}>"
388
+ end
389
+ end # Question
390
+ end # Prompt
391
+ end # TTY2
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
4
+ class Prompt
5
+ # Accumulates errors
6
+ class Result
7
+ attr_reader :question, :value, :errors
8
+
9
+ def initialize(question, value, errors = [])
10
+ @question = question
11
+ @value = value
12
+ @errors = errors
13
+ end
14
+
15
+ def with(condition = nil, &block)
16
+ validator = (condition || block)
17
+ (new_value, validation_error) = validator.call(question, value)
18
+ accumulated_errors = errors + Array(validation_error)
19
+
20
+ if accumulated_errors.empty?
21
+ Success.new(question, new_value)
22
+ else
23
+ Failure.new(question, new_value, accumulated_errors)
24
+ end
25
+ end
26
+
27
+ def success?
28
+ is_a?(Success)
29
+ end
30
+
31
+ def failure?
32
+ is_a?(Failure)
33
+ end
34
+
35
+ class Success < Result
36
+ end
37
+
38
+ class Failure < Result
39
+ end
40
+ end
41
+ end # Prompt
42
+ end # TTY2
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
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 # TTY2