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,317 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'choices'
4
+ require_relative 'paginator'
5
+ require_relative 'symbols'
6
+
7
+ module TTY
8
+ class Prompt
9
+ # A class responsible for rendering select list menu
10
+ # Used by {Prompt} to display interactive menu.
11
+ #
12
+ # @api private
13
+ class List
14
+ include Symbols
15
+
16
+ HELP = '(Use arrow%s keys, press Enter to select)'
17
+
18
+ PAGE_HELP = '(Move up or down to reveal more choices)'
19
+
20
+ # Create instance of TTY::Prompt::List menu.
21
+ #
22
+ # @param Hash options
23
+ # the configuration options
24
+ # @option options [Symbol] :default
25
+ # the default active choice, defaults to 1
26
+ # @option options [Symbol] :color
27
+ # the color for the selected item, defualts to :green
28
+ # @option options [Symbol] :marker
29
+ # the marker for the selected item
30
+ # @option options [String] :enum
31
+ # the delimiter for the item index
32
+ #
33
+ # @api public
34
+ def initialize(prompt, options = {})
35
+ @prompt = prompt
36
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
37
+ @enum = options.fetch(:enum) { nil }
38
+ @default = Array[options.fetch(:default) { 1 }]
39
+ @active = @default.first
40
+ @choices = Choices.new
41
+ @active_color = options.fetch(:active_color) { @prompt.active_color }
42
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
43
+ @marker = options.fetch(:marker) { symbols[:pointer] }
44
+ @help = options[:help]
45
+ @first_render = true
46
+ @done = false
47
+ @per_page = options[:per_page]
48
+ @page_help = options[:page_help] || PAGE_HELP
49
+ @paginator = Paginator.new
50
+
51
+ @prompt.subscribe(self)
52
+ end
53
+
54
+ # Set marker
55
+ #
56
+ # @api public
57
+ def marker(value)
58
+ @marker = value
59
+ end
60
+
61
+ # Set default option selected
62
+ #
63
+ # @api public
64
+ def default(*default_values)
65
+ @default = default_values
66
+ end
67
+
68
+ # Set number of items per page
69
+ #
70
+ # @api public
71
+ def per_page(value)
72
+ @per_page = value
73
+ end
74
+
75
+ def page_size
76
+ (@per_page || Paginator::DEFAULT_PAGE_SIZE)
77
+ end
78
+
79
+ # Check if list is paginated
80
+ #
81
+ # @return [Boolean]
82
+ #
83
+ # @api private
84
+ def paginated?
85
+ @choices.size > page_size
86
+ end
87
+
88
+ # @param [String] text
89
+ # the help text to display per page
90
+ # @api pbulic
91
+ def page_help(text)
92
+ @page_help = text
93
+ end
94
+
95
+ # Provide help information
96
+ #
97
+ # @param [String] value
98
+ # the new help text
99
+ #
100
+ # @return [String]
101
+ #
102
+ # @api public
103
+ def help(value = (not_set = true))
104
+ return @help if !@help.nil? && not_set
105
+
106
+ @help = (@help.nil? && !not_set) ? value : default_help
107
+ end
108
+
109
+ # Default help text
110
+ #
111
+ # @api public
112
+ def default_help
113
+ self.class::HELP % [enumerate? ? " or number (1-#{@choices.size})" : '']
114
+ end
115
+
116
+ # Set selecting active index using number pad
117
+ #
118
+ # @api public
119
+ def enum(value)
120
+ @enum = value
121
+ end
122
+
123
+ # Add a single choice
124
+ #
125
+ # @api public
126
+ def choice(*value, &block)
127
+ if block
128
+ @choices << (value << block)
129
+ else
130
+ @choices << value
131
+ end
132
+ end
133
+
134
+ # Add multiple choices
135
+ #
136
+ # @param [Array[Object]] values
137
+ # the values to add as choices
138
+ #
139
+ # @api public
140
+ def choices(values)
141
+ Array(values).each { |val| choice(*val) }
142
+ end
143
+
144
+ # Call the list menu by passing question and choices
145
+ #
146
+ # @param [String] question
147
+ #
148
+ # @param
149
+ # @api public
150
+ def call(question, possibilities, &block)
151
+ choices(possibilities)
152
+ @question = question
153
+ block.call(self) if block
154
+ setup_defaults
155
+ render
156
+ end
157
+
158
+ # Check if list is enumerated
159
+ #
160
+ # @return [Boolean]
161
+ def enumerate?
162
+ !@enum.nil?
163
+ end
164
+
165
+ def keynum(event)
166
+ return unless enumerate?
167
+ value = event.value.to_i
168
+ return unless (1..@choices.count).cover?(value)
169
+ @active = value
170
+ end
171
+
172
+ def keyspace(*)
173
+ @done = true
174
+ end
175
+
176
+ def keyreturn(*)
177
+ @done = true
178
+ end
179
+
180
+ def keyup(*)
181
+ @active = (@active == 1) ? @choices.length : @active - 1
182
+ end
183
+
184
+ def keydown(*)
185
+ @active = (@active == @choices.length) ? 1 : @active + 1
186
+ end
187
+ alias keytab keydown
188
+
189
+ private
190
+
191
+ # Setup default option and active selection
192
+ #
193
+ # @api private
194
+ def setup_defaults
195
+ validate_defaults
196
+ @active = @default.first
197
+ end
198
+
199
+ # Validate default indexes to be within range
200
+ #
201
+ # @api private
202
+ def validate_defaults
203
+ @default.each do |d|
204
+ if d.nil? || d.to_s.empty?
205
+ raise ConfigurationError,
206
+ "default index must be an integer in range (1 - #{@choices.size})"
207
+ end
208
+ if d < 1 || d > @choices.size
209
+ raise ConfigurationError,
210
+ "default index `#{d}` out of range (1 - #{@choices.size})"
211
+ end
212
+ end
213
+ end
214
+
215
+ # Render a selection list.
216
+ #
217
+ # By default the result is printed out.
218
+ #
219
+ # @return [Object] value
220
+ # return the selected value
221
+ #
222
+ # @api private
223
+ def render
224
+ @prompt.print(@prompt.hide)
225
+ until @done
226
+ question = render_question
227
+ @prompt.print(question)
228
+ @prompt.read_keypress
229
+ @prompt.print(refresh(question.lines.count))
230
+ end
231
+ @prompt.print(render_question)
232
+ answer
233
+ ensure
234
+ @prompt.print(@prompt.show)
235
+ end
236
+
237
+ # Find value for the choice selected
238
+ #
239
+ # @return [nil, Object]
240
+ #
241
+ # @api private
242
+ def answer
243
+ @choices[@active - 1].value
244
+ end
245
+
246
+ # Clear screen lines
247
+ #
248
+ # @param [String]
249
+ #
250
+ # @api private
251
+ def refresh(lines)
252
+ @prompt.clear_lines(lines)
253
+ end
254
+
255
+ # Render question with instructions and menu
256
+ #
257
+ # @return [String]
258
+ #
259
+ # @api private
260
+ def render_question
261
+ header = "#{@prefix}#{@question} #{render_header}\n"
262
+ @first_render = false
263
+ rendered_menu = render_menu
264
+ rendered_menu << render_footer
265
+ header << rendered_menu unless @done
266
+ header
267
+ end
268
+
269
+ # Render initial help and selected choice
270
+ #
271
+ # @return [String]
272
+ #
273
+ # @api private
274
+ def render_header
275
+ if @done
276
+ selected_item = "#{@choices[@active - 1].name}"
277
+ @prompt.decorate(selected_item, @active_color)
278
+ elsif @first_render
279
+ @prompt.decorate(help, @help_color)
280
+ end
281
+ end
282
+
283
+ # Render menu with choices to select from
284
+ #
285
+ # @return [String]
286
+ #
287
+ # @api private
288
+ def render_menu
289
+ output = ''
290
+ @paginator.paginate(@choices, @active, @per_page) do |choice, index|
291
+ num = enumerate? ? (index + 1).to_s + @enum + ' ' : ''
292
+ message = if index + 1 == @active
293
+ selected = @marker + ' ' + num + choice.name
294
+ @prompt.decorate("#{selected}", @active_color)
295
+ else
296
+ ' ' * 2 + num + choice.name
297
+ end
298
+ max_index = paginated? ? @paginator.max_index : @choices.size - 1
299
+ newline = (index == max_index) ? '' : "\n"
300
+ output << (message + newline)
301
+ end
302
+ output
303
+ end
304
+
305
+ # Render page info footer
306
+ #
307
+ # @return [String]
308
+ #
309
+ # @api private
310
+ def render_footer
311
+ return '' unless paginated?
312
+ colored_footer = @prompt.decorate(@page_help, @help_color)
313
+ "\n" << colored_footer
314
+ end
315
+ end # List
316
+ end # Prompt
317
+ end # TTY
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'question'
4
+ require_relative 'symbols'
5
+
6
+ module TTY
7
+ class Prompt
8
+ class MaskQuestion < Question
9
+ # Create masked question
10
+ #
11
+ # @param [Hash] options
12
+ # @option options [String] :mask
13
+ #
14
+ # @api public
15
+ def initialize(prompt, options = {})
16
+ super
17
+ @mask = options.fetch(:mask) { Symbols.symbols[:dot] }
18
+ @done_masked = false
19
+ @failure = false
20
+ @prompt.subscribe(self)
21
+ end
22
+
23
+ # Set character for masking the STDIN input
24
+ #
25
+ # @param [String] char
26
+ #
27
+ # @return [self]
28
+ #
29
+ # @api public
30
+ def mask(char = (not_set = true))
31
+ return @mask if not_set
32
+ @mask = char
33
+ end
34
+
35
+ def keyreturn(event)
36
+ @done_masked = true
37
+ end
38
+
39
+ def keyenter(event)
40
+ @done_masked = true
41
+ end
42
+
43
+ def keypress(event)
44
+ if [:backspace, :delete].include?(event.key.name)
45
+ @input.chop! unless @input.empty?
46
+ elsif event.value =~ /^[^\e\n\r]/
47
+ @input += event.value
48
+ end
49
+ end
50
+
51
+ # Render question and input replaced with masked character
52
+ #
53
+ # @api private
54
+ def render_question
55
+ header = "#{@prefix}#{message} "
56
+ if echo?
57
+ masked = "#{@mask * "#{@input}".length}"
58
+ if @done_masked && !@failure
59
+ masked = @prompt.decorate(masked, @active_color)
60
+ elsif @done_masked && @failure
61
+ masked = @prompt.decorate(masked, @error_color)
62
+ end
63
+ header += masked
64
+ end
65
+ header << "\n" if @done
66
+ header
67
+ end
68
+
69
+ def render_error(errors)
70
+ @failure = !errors.empty?
71
+ super
72
+ end
73
+
74
+ # Read input from user masked by character
75
+ #
76
+ # @private
77
+ def read_input(question)
78
+ @done_masked = false
79
+ @failure = false
80
+ @input = ''
81
+ until @done_masked
82
+ @prompt.read_keypress
83
+ @prompt.print(@prompt.clear_line)
84
+ @prompt.print(render_question)
85
+ end
86
+ @prompt.puts
87
+ @input
88
+ end
89
+ end # MaskQuestion
90
+ end # Prompt
91
+ end # TTY
@@ -0,0 +1,108 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'list'
4
+
5
+ module TTY
6
+ class Prompt
7
+ # A class responsible for rendering multi select list menu.
8
+ # Used by {Prompt} to display interactive choice menu.
9
+ #
10
+ # @api private
11
+ class MultiList < List
12
+ HELP = '(Use arrow%s keys, press Space to select and Enter to finish)'.freeze
13
+
14
+ # Create instance of TTY::Prompt::MultiList menu.
15
+ #
16
+ # @param [Prompt] :prompt
17
+ # @param [Hash] options
18
+ #
19
+ # @api public
20
+ def initialize(prompt, options)
21
+ super
22
+ @selected = []
23
+ @help = options[:help]
24
+ @default = Array(options[:default])
25
+ @echo = options.fetch(:echo, true)
26
+ end
27
+
28
+ # Callback fired when space key is pressed
29
+ #
30
+ # @api private
31
+ def keyspace(*)
32
+ active_choice = @choices[@active - 1]
33
+ if @selected.include?(active_choice)
34
+ @selected.delete(active_choice)
35
+ else
36
+ @selected << active_choice
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # Setup default options and active selection
43
+ #
44
+ # @api private
45
+ def setup_defaults
46
+ validate_defaults
47
+ @selected = @choices.values_at(*@default.map { |d| d - 1 })
48
+ @active = @default.last unless @selected.empty?
49
+ end
50
+
51
+ # Generate selected items names
52
+ #
53
+ # @return [String]
54
+ #
55
+ # @api private
56
+ def selected_names
57
+ @selected.map(&:name).join(', ')
58
+ end
59
+
60
+ # Render initial help text and then currently selected choices
61
+ #
62
+ # @api private
63
+ def render_header
64
+ instructions = @prompt.decorate(help, :bright_black)
65
+ if @done && @echo
66
+ @prompt.decorate(selected_names, @active_color)
67
+ elsif @selected.size.nonzero? && @echo
68
+ selected_names + (@first_render ? " #{instructions}" : '')
69
+ elsif @first_render
70
+ instructions
71
+ end
72
+ end
73
+
74
+ # All values for the choices selected
75
+ #
76
+ # @return [Array[nil,Object]]
77
+ #
78
+ # @api private
79
+ def answer
80
+ @selected.map(&:value)
81
+ end
82
+
83
+ # Render menu with choices to select from
84
+ #
85
+ # @return [String]
86
+ #
87
+ # @api private
88
+ def render_menu
89
+ output = ''
90
+ @paginator.paginate(@choices, @active, @per_page) do |choice, index|
91
+ num = enumerate? ? (index + 1).to_s + @enum + ' ' : ''
92
+ indicator = (index + 1 == @active) ? @marker : ' '
93
+ indicator += ' '
94
+ message = if @selected.include?(choice)
95
+ selected = @prompt.decorate(symbols[:radio_on], @active_color)
96
+ selected + ' ' + num + choice.name
97
+ else
98
+ symbols[:radio_off] + ' ' + num + choice.name
99
+ end
100
+ max_index = paginated? ? @paginator.max_index : @choices.size - 1
101
+ newline = (index == max_index) ? '' : "\n"
102
+ output << indicator + message + newline
103
+ end
104
+ output
105
+ end
106
+ end # MultiList
107
+ end # Prompt
108
+ end # TTY