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,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