austb-tty-prompt 0.13.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 (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +25 -0
  5. data/CHANGELOG.md +218 -0
  6. data/CODE_OF_CONDUCT.md +49 -0
  7. data/Gemfile +19 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +1132 -0
  10. data/Rakefile +8 -0
  11. data/appveyor.yml +23 -0
  12. data/benchmarks/speed.rb +27 -0
  13. data/examples/ask.rb +15 -0
  14. data/examples/collect.rb +19 -0
  15. data/examples/echo.rb +11 -0
  16. data/examples/enum.rb +8 -0
  17. data/examples/enum_paged.rb +9 -0
  18. data/examples/enum_select.rb +7 -0
  19. data/examples/expand.rb +29 -0
  20. data/examples/in.rb +9 -0
  21. data/examples/inputs.rb +10 -0
  22. data/examples/key_events.rb +11 -0
  23. data/examples/keypress.rb +9 -0
  24. data/examples/mask.rb +13 -0
  25. data/examples/multi_select.rb +8 -0
  26. data/examples/multi_select_paged.rb +9 -0
  27. data/examples/multiline.rb +9 -0
  28. data/examples/pause.rb +7 -0
  29. data/examples/select.rb +18 -0
  30. data/examples/select_paginated.rb +9 -0
  31. data/examples/slider.rb +6 -0
  32. data/examples/validation.rb +9 -0
  33. data/examples/yes_no.rb +7 -0
  34. data/lib/tty-prompt.rb +4 -0
  35. data/lib/tty/prompt.rb +535 -0
  36. data/lib/tty/prompt/answers_collector.rb +59 -0
  37. data/lib/tty/prompt/choice.rb +90 -0
  38. data/lib/tty/prompt/choices.rb +110 -0
  39. data/lib/tty/prompt/confirm_question.rb +129 -0
  40. data/lib/tty/prompt/converter_dsl.rb +22 -0
  41. data/lib/tty/prompt/converter_registry.rb +64 -0
  42. data/lib/tty/prompt/converters.rb +77 -0
  43. data/lib/tty/prompt/distance.rb +49 -0
  44. data/lib/tty/prompt/enum_list.rb +337 -0
  45. data/lib/tty/prompt/enum_paginator.rb +56 -0
  46. data/lib/tty/prompt/evaluator.rb +29 -0
  47. data/lib/tty/prompt/expander.rb +292 -0
  48. data/lib/tty/prompt/keypress.rb +94 -0
  49. data/lib/tty/prompt/list.rb +317 -0
  50. data/lib/tty/prompt/mask_question.rb +91 -0
  51. data/lib/tty/prompt/multi_list.rb +108 -0
  52. data/lib/tty/prompt/multiline.rb +71 -0
  53. data/lib/tty/prompt/paginator.rb +88 -0
  54. data/lib/tty/prompt/question.rb +333 -0
  55. data/lib/tty/prompt/question/checks.rb +87 -0
  56. data/lib/tty/prompt/question/modifier.rb +94 -0
  57. data/lib/tty/prompt/question/validation.rb +72 -0
  58. data/lib/tty/prompt/reader.rb +352 -0
  59. data/lib/tty/prompt/reader/codes.rb +121 -0
  60. data/lib/tty/prompt/reader/console.rb +57 -0
  61. data/lib/tty/prompt/reader/history.rb +145 -0
  62. data/lib/tty/prompt/reader/key_event.rb +91 -0
  63. data/lib/tty/prompt/reader/line.rb +162 -0
  64. data/lib/tty/prompt/reader/mode.rb +44 -0
  65. data/lib/tty/prompt/reader/win_api.rb +29 -0
  66. data/lib/tty/prompt/reader/win_console.rb +53 -0
  67. data/lib/tty/prompt/result.rb +42 -0
  68. data/lib/tty/prompt/slider.rb +182 -0
  69. data/lib/tty/prompt/statement.rb +55 -0
  70. data/lib/tty/prompt/suggestion.rb +115 -0
  71. data/lib/tty/prompt/symbols.rb +61 -0
  72. data/lib/tty/prompt/timeout.rb +69 -0
  73. data/lib/tty/prompt/utils.rb +44 -0
  74. data/lib/tty/prompt/version.rb +7 -0
  75. data/lib/tty/test_prompt.rb +20 -0
  76. data/tasks/console.rake +11 -0
  77. data/tasks/coverage.rake +11 -0
  78. data/tasks/spec.rake +29 -0
  79. data/tty-prompt.gemspec +32 -0
  80. metadata +243 -0
@@ -0,0 +1,77 @@
1
+ # encoding: utf-8
2
+
3
+ require 'pathname'
4
+ require 'necromancer'
5
+
6
+ require_relative 'converter_dsl'
7
+
8
+ module TTY
9
+ class Prompt
10
+ module Converters
11
+ extend ConverterDSL
12
+
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.'
21
+ 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
+ end
29
+
30
+ converter(:string) do |input|
31
+ String(input).chomp
32
+ end
33
+
34
+ converter(:symbol) do |input|
35
+ input.to_sym
36
+ end
37
+
38
+ converter(:date) do |input|
39
+ on_error { Necromancer.convert(input).to(:date, strict: true) }
40
+ end
41
+
42
+ converter(:datetime) do |input|
43
+ on_error { Necromancer.convert(input).to(:datetime, strict: true) }
44
+ end
45
+
46
+ converter(:int) do |input|
47
+ on_error { Necromancer.convert(input).to(:integer, strict: true) }
48
+ end
49
+
50
+ converter(:float) do |input|
51
+ on_error { Necromancer.convert(input).to(:float, strict: true) }
52
+ end
53
+
54
+ converter(:range) do |input|
55
+ on_error { Necromancer.convert(input).to(:range, strict: true) }
56
+ end
57
+
58
+ converter(:regexp) do |input|
59
+ Regexp.new(input)
60
+ end
61
+
62
+ converter(:file) do |input|
63
+ directory = ::File.expand_path(::File.dirname($0))
64
+ ::File.open(::File.join(directory, input))
65
+ end
66
+
67
+ converter(:path) do |input|
68
+ directory = ::File.expand_path(::File.dirname($0))
69
+ Pathname.new(::File.join(directory, input))
70
+ end
71
+
72
+ converter(:char) do |input|
73
+ String(input).chars.to_a[0]
74
+ end
75
+ end # Converters
76
+ end # Prompt
77
+ end # TTY
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ # A class responsible for string comparison
6
+ class Distance
7
+ # Calculate the optimal string alignment distance
8
+ #
9
+ # @api public
10
+ def distance(first, second)
11
+ distances = []
12
+ rows = first.to_s.length
13
+ cols = second.to_s.length
14
+
15
+ 0.upto(rows) do |index|
16
+ distances << [index] + [0] * cols
17
+ end
18
+ distances[0] = 0.upto(cols).to_a
19
+
20
+ 1.upto(rows) do |first_index|
21
+ 1.upto(cols) do |second_index|
22
+ first_char = first[first_index - 1]
23
+ second_char = second[second_index - 1]
24
+ cost = first_char == second_char ? 0 : 1
25
+
26
+ distances[first_index][second_index] = [
27
+ distances[first_index - 1][second_index], # deletion
28
+ distances[first_index][second_index - 1], # insertion
29
+ distances[first_index - 1][second_index - 1] # substitution
30
+ ].min + cost
31
+
32
+ if first_index > 1 && second_index > 1
33
+ first_previous_char = first[first_index - 2]
34
+ second_previous_char = second[second_index - 2]
35
+ if first_char == second_previous_char && second_char == first_previous_char
36
+ distances[first_index][second_index] = [
37
+ distances[first_index][second_index],
38
+ distances[first_index - 2][second_index - 2] + 1 # transposition
39
+ ].min
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+ distances[rows][cols]
46
+ end
47
+ end # Distance
48
+ end # Prompt
49
+ end # TTY
@@ -0,0 +1,337 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'choices'
4
+ require_relative 'enum_paginator'
5
+ require_relative 'paginator'
6
+
7
+ module TTY
8
+ class Prompt
9
+ # A class reponsible for rendering enumerated list menu.
10
+ # Used by {Prompt} to display static choice menu.
11
+ #
12
+ # @api private
13
+ class EnumList
14
+ PAGE_HELP = '(Press tab/right or left to reveal more choices)'.freeze
15
+
16
+ # Create instance of EnumList menu.
17
+ #
18
+ # @api public
19
+ def initialize(prompt, options = {})
20
+ @prompt = prompt
21
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
22
+ @enum = options.fetch(:enum) { ')' }
23
+ @default = options.fetch(:default) { 1 }
24
+ @active_color = options.fetch(:active_color) { @prompt.active_color }
25
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
26
+ @error_color = options.fetch(:error_color) { @prompt.error_color }
27
+ @input = nil
28
+ @done = false
29
+ @first_render = true
30
+ @failure = false
31
+ @active = @default
32
+ @choices = Choices.new
33
+ @per_page = options[:per_page]
34
+ @page_help = options[:page_help] || PAGE_HELP
35
+ @paginator = EnumPaginator.new
36
+ @page_active = @default
37
+
38
+ @prompt.subscribe(self)
39
+ end
40
+
41
+ # Set default option selected
42
+ #
43
+ # @api public
44
+ def default(default)
45
+ @default = default
46
+ end
47
+
48
+ # Set number of items per page
49
+ #
50
+ # @api public
51
+ def per_page(value)
52
+ @per_page = value
53
+ end
54
+
55
+ def page_size
56
+ (@per_page || Paginator::DEFAULT_PAGE_SIZE)
57
+ end
58
+
59
+ # Check if list is paginated
60
+ #
61
+ # @return [Boolean]
62
+ #
63
+ # @api private
64
+ def paginated?
65
+ @choices.size > page_size
66
+ end
67
+
68
+ # @param [String] text
69
+ # the help text to display per page
70
+ # @api pbulic
71
+ def page_help(text)
72
+ @page_help = text
73
+ end
74
+
75
+ # Set selecting active index using number pad
76
+ #
77
+ # @api public
78
+ def enum(value)
79
+ @enum = value
80
+ end
81
+
82
+ # Add a single choice
83
+ #
84
+ # @api public
85
+ def choice(*value, &block)
86
+ if block
87
+ @choices << (value << block)
88
+ else
89
+ @choices << value
90
+ end
91
+ end
92
+
93
+ # Add multiple choices
94
+ #
95
+ # @param [Array[Object]] values
96
+ # the values to add as choices
97
+ #
98
+ # @api public
99
+ def choices(values)
100
+ values.each { |val| choice(*val) }
101
+ end
102
+
103
+ # Call the list menu by passing question and choices
104
+ #
105
+ # @param [String] question
106
+ #
107
+ # @param
108
+ # @api public
109
+ def call(question, possibilities, &block)
110
+ choices(possibilities)
111
+ @question = question
112
+ block[self] if block
113
+ setup_defaults
114
+ render
115
+ end
116
+
117
+ def keypress(event)
118
+ if [:backspace, :delete].include?(event.key.name)
119
+ return if @input.empty?
120
+ @input.chop!
121
+ mark_choice_as_active
122
+ elsif event.value =~ /^\d+$/
123
+ @input += event.value
124
+ mark_choice_as_active
125
+ end
126
+ end
127
+
128
+ def keyreturn(*)
129
+ @failure = false
130
+ if (@input.to_i > 0 && @input.to_i <= @choices.size) || @input.empty?
131
+ @done = true
132
+ else
133
+ @input = ''
134
+ @failure = true
135
+ end
136
+ end
137
+ alias keyenter keyreturn
138
+
139
+ def keyright(*)
140
+ if (@page_active + page_size) <= @choices.size
141
+ @page_active += page_size
142
+ else
143
+ @page_active = 1
144
+ end
145
+ end
146
+ alias keytab keyright
147
+
148
+ def keyleft(*)
149
+ if (@page_active - page_size) >= 0
150
+ @page_active -= page_size
151
+ else
152
+ @page_active = @choices.size - 1
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ # Find active choice or set to default
159
+ #
160
+ # @return [nil]
161
+ #
162
+ # @api private
163
+ def mark_choice_as_active
164
+ if (@input.to_i > 0) && !@choices[@input.to_i - 1].nil?
165
+ @active = @input.to_i
166
+ else
167
+ @active = @default
168
+ end
169
+ @page_active = @active
170
+ end
171
+
172
+ # Validate default indexes to be within range
173
+ #
174
+ # @api private
175
+ def validate_defaults
176
+ return if @default >= 1 && @default <= @choices.size
177
+ raise PromptConfigurationError,
178
+ "default index `#{d}` out of range (1 - #{@choices.size})"
179
+ end
180
+
181
+ # Setup default option and active selection
182
+ #
183
+ # @api private
184
+ def setup_defaults
185
+ validate_defaults
186
+ mark_choice_as_active
187
+ end
188
+
189
+ # Render a selection list.
190
+ #
191
+ # By default the result is printed out.
192
+ #
193
+ # @return [Object] value
194
+ # return the selected value
195
+ #
196
+ # @api private
197
+ def render
198
+ @input = ''
199
+ until @done
200
+ question = render_question
201
+ @prompt.print(question)
202
+ @prompt.print(render_error) if @failure
203
+ if paginated? && !@done
204
+ @prompt.print(render_page_help)
205
+ end
206
+ @prompt.read_keypress
207
+ @prompt.print(refresh(question.lines.count))
208
+ end
209
+ @prompt.print(render_question)
210
+ answer
211
+ end
212
+
213
+ # Find value for the choice selected
214
+ #
215
+ # @return [nil, Object]
216
+ #
217
+ # @api private
218
+ def answer
219
+ @choices[@active - 1].value
220
+ end
221
+
222
+ # Determine area of the screen to clear
223
+ #
224
+ # @param [Integer] lines
225
+ # the lines to clear
226
+ #
227
+ # @return [String]
228
+ #
229
+ # @api private
230
+ def refresh(lines)
231
+ @prompt.clear_lines(lines) +
232
+ @prompt.cursor.clear_screen_down
233
+ end
234
+
235
+ # Render question with the menu options
236
+ #
237
+ # @return [String]
238
+ #
239
+ # @api private
240
+ def render_question
241
+ header = "#{@prefix}#{@question} #{render_header}\n"
242
+ unless @done
243
+ header << render_menu
244
+ header << render_footer
245
+ end
246
+ header
247
+ end
248
+
249
+ # Error message when incorrect index chosen
250
+ #
251
+ # @api private
252
+ def error_message
253
+ error = 'Please enter a valid number'
254
+ "\n" + @prompt.decorate('>>', @error_color) + ' ' + error
255
+ end
256
+
257
+ # Render error message and return cursor to position of input
258
+ #
259
+ # @return [String]
260
+ #
261
+ # @api private
262
+ def render_error
263
+ error = error_message.dup
264
+ if !paginated?
265
+ error << @prompt.cursor.prev_line
266
+ error << @prompt.cursor.forward(render_footer.size)
267
+ end
268
+ error
269
+ end
270
+
271
+ # Render chosen option
272
+ #
273
+ # @return [String]
274
+ #
275
+ # @api private
276
+ def render_header
277
+ return '' unless @done
278
+ return '' unless @active
279
+ selected_item = @choices[@active - 1].name.to_s
280
+ @prompt.decorate(selected_item, @active_color)
281
+ end
282
+
283
+ # Render footer for the indexed menu
284
+ #
285
+ # @return [String]
286
+ #
287
+ # @api private
288
+ def render_footer
289
+ " Choose 1-#{@choices.size} [#{@default}]: #{@input}"
290
+ end
291
+
292
+ # Pagination help message
293
+ #
294
+ # @return [String]
295
+ #
296
+ # @api private
297
+ def page_help_message
298
+ return '' unless paginated?
299
+ "\n" + @prompt.decorate(@page_help, @help_color)
300
+ end
301
+
302
+ # Render page help
303
+ #
304
+ # @return [String]
305
+ #
306
+ # @api private
307
+ def render_page_help
308
+ help = page_help_message.dup
309
+ if @failure
310
+ help << @prompt.cursor.prev_line
311
+ end
312
+ help << @prompt.cursor.prev_line
313
+ help << @prompt.cursor.forward(render_footer.size)
314
+ end
315
+
316
+ # Render menu with indexed choices to select from
317
+ #
318
+ # @return [String]
319
+ #
320
+ # @api private
321
+ def render_menu
322
+ output = ''
323
+ @paginator.paginate(@choices, @page_active, @per_page) do |choice, index|
324
+ num = (index + 1).to_s + @enum + ' '
325
+ selected = ' ' * 2 + num + choice.name
326
+ output << if index + 1 == @active
327
+ @prompt.decorate(selected.to_s, @active_color)
328
+ else
329
+ selected
330
+ end
331
+ output << "\n"
332
+ end
333
+ output
334
+ end
335
+ end # EnumList
336
+ end # Prompt
337
+ end # TTY