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,433 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+
5
+ require_relative "choices"
6
+ require_relative "block_paginator"
7
+ require_relative "paginator"
8
+
9
+ module TTY2
10
+ class Prompt
11
+ # A class reponsible for rendering enumerated list menu.
12
+ # Used by {Prompt} to display static choice menu.
13
+ #
14
+ # @api private
15
+ class EnumList
16
+ PAGE_HELP = "(Press tab/right or left to reveal more choices)"
17
+
18
+ # Checks type of default parameter to be integer
19
+ INTEGER_MATCHER = /\A[-+]?\d+\Z/.freeze
20
+
21
+ # Create instance of EnumList menu.
22
+ #
23
+ # @api public
24
+ def initialize(prompt, **options)
25
+ @prompt = prompt
26
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
27
+ @enum = options.fetch(:enum) { ")" }
28
+ @default = options.fetch(:default, nil)
29
+ @active_color = options.fetch(:active_color) { @prompt.active_color }
30
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
31
+ @error_color = options.fetch(:error_color) { @prompt.error_color }
32
+ @cycle = options.fetch(:cycle, false)
33
+ @quiet = options.fetch(:quiet) { @prompt.quiet }
34
+ @symbols = @prompt.symbols.merge(options.fetch(:symbols, {}))
35
+ @input = nil
36
+ @done = false
37
+ @first_render = true
38
+ @failure = false
39
+ @active = @default
40
+ @choices = Choices.new
41
+ @per_page = options[:per_page]
42
+ @page_help = options[:page_help] || PAGE_HELP
43
+ @paginator = BlockPaginator.new
44
+ @page_active = @default
45
+ end
46
+
47
+ # Change symbols used by this prompt
48
+ #
49
+ # @param [Hash] new_symbols
50
+ # the new symbols to use
51
+ #
52
+ # @api public
53
+ def symbols(new_symbols = (not_set = true))
54
+ return @symbols if not_set
55
+
56
+ @symbols.merge!(new_symbols)
57
+ end
58
+
59
+ # Set default option selected
60
+ #
61
+ # @api public
62
+ def default(default)
63
+ @default = default
64
+ end
65
+
66
+ # Check if default value is set
67
+ #
68
+ # @return [Boolean]
69
+ #
70
+ # @api public
71
+ def default?
72
+ !@default.to_s.empty?
73
+ end
74
+
75
+ # Set number of items per page
76
+ #
77
+ # @api public
78
+ def per_page(value)
79
+ @per_page = value
80
+ end
81
+
82
+ def page_size
83
+ (@per_page || Paginator::DEFAULT_PAGE_SIZE)
84
+ end
85
+
86
+ # Check if list is paginated
87
+ #
88
+ # @return [Boolean]
89
+ #
90
+ # @api private
91
+ def paginated?
92
+ @choices.size > page_size
93
+ end
94
+
95
+ # @param [String] text
96
+ # the help text to display per page
97
+ # @api pbulic
98
+ def page_help(text)
99
+ @page_help = text
100
+ end
101
+
102
+ # Set selecting active index using number pad
103
+ #
104
+ # @api public
105
+ def enum(value)
106
+ @enum = value
107
+ end
108
+
109
+ # Set quiet mode
110
+ #
111
+ # @api public
112
+ def quiet(value)
113
+ @quiet = value
114
+ end
115
+
116
+ # Add a single choice
117
+ #
118
+ # @api public
119
+ def choice(*value, &block)
120
+ if block
121
+ @choices << (value << block)
122
+ else
123
+ @choices << value
124
+ end
125
+ end
126
+
127
+ # Add multiple choices
128
+ #
129
+ # @param [Array[Object]] values
130
+ # the values to add as choices
131
+ #
132
+ # @api public
133
+ def choices(values = (not_set = true))
134
+ if not_set
135
+ @choices
136
+ else
137
+ values.each { |val| @choices << val }
138
+ end
139
+ end
140
+
141
+ # Call the list menu by passing question and choices
142
+ #
143
+ # @param [String] question
144
+ #
145
+ # @param
146
+ # @api public
147
+ def call(question, possibilities, &block)
148
+ choices(possibilities)
149
+ @question = question
150
+ block[self] if block
151
+ setup_defaults
152
+ @prompt.subscribe(self) do
153
+ render
154
+ end
155
+ end
156
+
157
+ def keypress(event)
158
+ if %i[backspace delete].include?(event.key.name)
159
+ return if @input.empty?
160
+
161
+ @input.chop!
162
+ mark_choice_as_active
163
+ elsif event.value =~ /^\d+$/
164
+ @input += event.value
165
+ mark_choice_as_active
166
+ end
167
+ end
168
+
169
+ def keyreturn(*)
170
+ @failure = false
171
+ num = @input.to_i
172
+ choice_disabled = choices[num - 1] && choices[num - 1].disabled?
173
+ choice_in_range = num > 0 && num <= @choices.size
174
+
175
+ if choice_in_range && !choice_disabled || @input.empty?
176
+ @done = true
177
+ else
178
+ @input = ""
179
+ @failure = true
180
+ end
181
+ end
182
+ alias keyenter keyreturn
183
+
184
+ def keyright(*)
185
+ if (@page_active + page_size) <= @choices.size
186
+ @page_active += page_size
187
+ elsif @cycle
188
+ @page_active = 1
189
+ end
190
+ end
191
+ alias keytab keyright
192
+
193
+ def keyleft(*)
194
+ if (@page_active - page_size) >= 0
195
+ @page_active -= page_size
196
+ elsif @cycle
197
+ @page_active = @choices.size - 1
198
+ end
199
+ end
200
+
201
+ private
202
+
203
+ # Find active choice or set to default
204
+ #
205
+ # @return [nil]
206
+ #
207
+ # @api private
208
+ def mark_choice_as_active
209
+ next_active = @choices[@input.to_i - 1]
210
+
211
+ if next_active && next_active.disabled?
212
+ # noop
213
+ elsif (@input.to_i > 0) && next_active
214
+ @active = @input.to_i
215
+ else
216
+ @active = @default
217
+ end
218
+ @page_active = @active
219
+ end
220
+
221
+ # Validate default indexes to be within range
222
+ #
223
+ # @api private
224
+ def validate_defaults
225
+ msg = if @default.nil? || @default.to_s.empty?
226
+ "default index must be an integer in range (1 - #{choices.size})"
227
+ elsif @default.to_s !~ INTEGER_MATCHER
228
+ validate_default_name
229
+ elsif @default < 1 || @default > @choices.size
230
+ "default index #{@default} out of range (1 - #{@choices.size})"
231
+ elsif choices[@default - 1] && choices[@default - 1].disabled?
232
+ "default index #{@default} matches disabled choice item"
233
+ end
234
+
235
+ raise(ConfigurationError, msg) if msg
236
+ end
237
+
238
+ # Validate default choice name
239
+ #
240
+ # @return [String]
241
+ #
242
+ # @api private
243
+ def validate_default_name
244
+ default_choice = choices.find_by(:name, @default.to_s)
245
+ if default_choice.nil?
246
+ "no choice found for the default name: #{@default.inspect}"
247
+ elsif default_choice.disabled?
248
+ "default name #{@default.inspect} matches disabled choice"
249
+ end
250
+ end
251
+
252
+ # Setup default option and active selection
253
+ #
254
+ # @api private
255
+ def setup_defaults
256
+ if @default.to_s.empty?
257
+ @default = (0..choices.length).find { |i| !choices[i].disabled? } + 1
258
+ end
259
+ validate_defaults
260
+ if default_choice = choices.find_by(:name, @default)
261
+ @default = choices.index(default_choice) + 1
262
+ end
263
+ mark_choice_as_active
264
+ end
265
+
266
+ # Render a selection list.
267
+ #
268
+ # By default the result is printed out.
269
+ #
270
+ # @return [Object] value
271
+ # return the selected value
272
+ #
273
+ # @api private
274
+ def render
275
+ @input = ""
276
+ until @done
277
+ question = render_question
278
+ @prompt.print(question)
279
+ @prompt.print(render_error) if @failure
280
+ if paginated? && !@done
281
+ @prompt.print(render_page_help)
282
+ end
283
+ @prompt.read_keypress
284
+ question_lines = question.split($INPUT_RECORD_SEPARATOR, -1)
285
+ @prompt.print(refresh(question_lines_count(question_lines)))
286
+ end
287
+ @prompt.print(render_question) unless @quiet
288
+ answer
289
+ end
290
+
291
+ # Count how many screen lines the question spans
292
+ #
293
+ # @return [Integer]
294
+ #
295
+ # @api private
296
+ def question_lines_count(question_lines)
297
+ question_lines.reduce(0) do |acc, line|
298
+ acc + @prompt.count_screen_lines(line)
299
+ end
300
+ end
301
+
302
+ # Find value for the choice selected
303
+ #
304
+ # @return [nil, Object]
305
+ #
306
+ # @api private
307
+ def answer
308
+ @choices[@active - 1].value
309
+ end
310
+
311
+ # Determine area of the screen to clear
312
+ #
313
+ # @param [Integer] lines
314
+ # the lines to clear
315
+ #
316
+ # @return [String]
317
+ #
318
+ # @api private
319
+ def refresh(lines)
320
+ @prompt.clear_lines(lines) +
321
+ @prompt.cursor.clear_screen_down
322
+ end
323
+
324
+ # Render question with the menu options
325
+ #
326
+ # @return [String]
327
+ #
328
+ # @api private
329
+ def render_question
330
+ header = ["#{@prefix}#{@question} #{render_header}\n"]
331
+ unless @done
332
+ header << render_menu
333
+ header << render_footer
334
+ end
335
+ header.join
336
+ end
337
+
338
+ # Error message when incorrect index chosen
339
+ #
340
+ # @api private
341
+ def error_message
342
+ error = "Please enter a valid number"
343
+ "\n" + @prompt.decorate(">>", @error_color) + " " + error
344
+ end
345
+
346
+ # Render error message and return cursor to position of input
347
+ #
348
+ # @return [String]
349
+ #
350
+ # @api private
351
+ def render_error
352
+ error = error_message.dup
353
+ if !paginated?
354
+ error << @prompt.cursor.prev_line
355
+ error << @prompt.cursor.forward(render_footer.size)
356
+ end
357
+ error
358
+ end
359
+
360
+ # Render chosen option
361
+ #
362
+ # @return [String]
363
+ #
364
+ # @api private
365
+ def render_header
366
+ return "" unless @done
367
+ return "" unless @active
368
+
369
+ selected_item = @choices[@active - 1].name.to_s
370
+ @prompt.decorate(selected_item, @active_color)
371
+ end
372
+
373
+ # Render footer for the indexed menu
374
+ #
375
+ # @return [String]
376
+ #
377
+ # @api private
378
+ def render_footer
379
+ " Choose 1-#{@choices.size} [#{@default}]: #{@input}"
380
+ end
381
+
382
+ # Pagination help message
383
+ #
384
+ # @return [String]
385
+ #
386
+ # @api private
387
+ def page_help_message
388
+ return "" unless paginated?
389
+
390
+ "\n" + @prompt.decorate(@page_help, @help_color)
391
+ end
392
+
393
+ # Render page help
394
+ #
395
+ # @return [String]
396
+ #
397
+ # @api private
398
+ def render_page_help
399
+ help = page_help_message.dup
400
+ if @failure
401
+ help << @prompt.cursor.prev_line
402
+ end
403
+ help << @prompt.cursor.prev_line
404
+ help << @prompt.cursor.forward(render_footer.size)
405
+ end
406
+
407
+ # Render menu with indexed choices to select from
408
+ #
409
+ # @return [String]
410
+ #
411
+ # @api private
412
+ def render_menu
413
+ output = []
414
+
415
+ @paginator.paginate(@choices, @page_active, @per_page) do |choice, index|
416
+ num = (index + 1).to_s + @enum + " "
417
+ selected = num.to_s + choice.name.to_s
418
+ output << if index + 1 == @active && !choice.disabled?
419
+ (" " * 2) + @prompt.decorate(selected, @active_color)
420
+ elsif choice.disabled?
421
+ @prompt.decorate(@symbols[:cross], :red) + " " +
422
+ selected + " " + choice.disabled.to_s
423
+ else
424
+ (" " * 2) + selected
425
+ end
426
+ output << "\n"
427
+ end
428
+
429
+ output.join
430
+ end
431
+ end # EnumList
432
+ end # Prompt
433
+ end # TTY2
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY2
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 # TTY2
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "result"
4
+
5
+ module TTY2
6
+ class Prompt
7
+ # Evaluates provided parameters and stops if any of them fails
8
+ # @api private
9
+ class Evaluator
10
+ attr_reader :results
11
+
12
+ def initialize(question, &block)
13
+ @question = question
14
+ @results = []
15
+ instance_eval(&block) if block
16
+ end
17
+
18
+ def call(initial)
19
+ seed = Result::Success.new(@question, initial)
20
+ results.reduce(seed, &:with)
21
+ end
22
+
23
+ def check(proc = nil, &block)
24
+ results << (proc || block)
25
+ end
26
+ alias << check
27
+ end # Evaluator
28
+ end # Prompt
29
+ end # TTY2