austb-tty-prompt 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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