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,71 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'question'
4
+ require_relative 'symbols'
5
+
6
+ module TTY
7
+ class Prompt
8
+ # A prompt responsible for multi line user input
9
+ #
10
+ # @api private
11
+ class Multiline < Question
12
+ HELP = '(Press CTRL-D or CTRL-Z to finish)'.freeze
13
+
14
+ def initialize(prompt, options = {})
15
+ super
16
+ @help = options[:help] || self.class::HELP
17
+ @first_render = true
18
+ @lines_count = 0
19
+
20
+ @prompt.subscribe(self)
21
+ end
22
+
23
+ # Provide help information
24
+ #
25
+ # @return [String]
26
+ #
27
+ # @api public
28
+ def help(value = (not_set = true))
29
+ return @help if not_set
30
+ @help = value
31
+ end
32
+
33
+ def read_input
34
+ @prompt.read_multiline
35
+ end
36
+
37
+ def keyreturn(*)
38
+ @lines_count += 1
39
+ end
40
+ alias keyenter keyreturn
41
+
42
+ def render_question
43
+ header = "#{@prefix}#{message} "
44
+ if !echo?
45
+ header
46
+ elsif @done
47
+ header += @prompt.decorate("#{@input}", @active_color)
48
+ elsif @first_render
49
+ header += @prompt.decorate(help, @help_color)
50
+ @first_render = false
51
+ end
52
+ header += "\n"
53
+ header
54
+ end
55
+
56
+ def process_input(question)
57
+ @lines = read_input
58
+ @input = "#{@lines.first.strip} ..." unless @lines.first.to_s.empty?
59
+ if Utils.blank?(@input)
60
+ @input = default? ? default : nil
61
+ end
62
+ @evaluator.(@lines)
63
+ end
64
+
65
+ def refresh(lines)
66
+ size = @lines_count + lines + 1
67
+ @prompt.clear_lines(size)
68
+ end
69
+ end # Multiline
70
+ end # Prompt
71
+ end # TTY
@@ -0,0 +1,88 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ class Paginator
6
+ DEFAULT_PAGE_SIZE = 6
7
+
8
+ # Create a Paginator
9
+ #
10
+ # @api private
11
+ def initialize(options = {})
12
+ @last_index = Array(options[:default]).flatten.first || 0
13
+ @per_page = options[:per_page]
14
+ @lower_index = Array(options[:default]).flatten.first
15
+ end
16
+
17
+ # Maximum index for current pagination
18
+ #
19
+ # @return [Integer]
20
+ #
21
+ # @api public
22
+ def max_index
23
+ raise ArgumentError, 'no max index' unless @per_page
24
+ @lower_index + @per_page - 1
25
+ end
26
+
27
+ # Paginate collection given an active index
28
+ #
29
+ # @param [Array[Choice]] list
30
+ # a collection of choice items
31
+ # @param [Integer] active
32
+ # current choice active index
33
+ # @param [Integer] per_page
34
+ # number of choice items per page
35
+ #
36
+ # @return [Enumerable]
37
+ #
38
+ # @api public
39
+ def paginate(list, active, per_page = nil, &block)
40
+ current_index = active - 1
41
+ default_size = (list.size <= DEFAULT_PAGE_SIZE ? list.size : DEFAULT_PAGE_SIZE)
42
+ @per_page = @per_page || per_page || default_size
43
+ @lower_index ||= current_index
44
+ @upper_index ||= max_index
45
+
46
+ # Don't paginate short lists
47
+ if list.size <= @per_page
48
+ @lower_index = 0
49
+ @upper_index = list.size - 1
50
+ if block
51
+ return list.each_with_index(&block)
52
+ else
53
+ return list.each_with_index.to_enum
54
+ end
55
+ end
56
+
57
+ if current_index > @last_index # going up
58
+ if current_index > @upper_index && current_index < list.size - 1
59
+ @lower_index += 1
60
+ end
61
+ elsif current_index < @last_index # going down
62
+ if current_index < @lower_index && current_index > 0
63
+ @lower_index -= 1
64
+ end
65
+ end
66
+
67
+ # Cycle list
68
+ if current_index.zero?
69
+ @lower_index = 0
70
+ elsif current_index == list.size - 1
71
+ @lower_index = list.size - 1 - (@per_page - 1)
72
+ end
73
+
74
+ @upper_index = @lower_index + (@per_page - 1)
75
+ @last_index = current_index
76
+
77
+ sliced_list = list[@lower_index..@upper_index]
78
+ indices = (@lower_index..@upper_index)
79
+
80
+ return sliced_list.zip(indices).to_enum unless block_given?
81
+
82
+ sliced_list.each_with_index do |item, index|
83
+ block[item, @lower_index + index]
84
+ end
85
+ end
86
+ end # Paginator
87
+ end # Prompt
88
+ end # TTY
@@ -0,0 +1,333 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative 'converters'
4
+ require_relative 'evaluator'
5
+ require_relative 'question/modifier'
6
+ require_relative 'question/validation'
7
+ require_relative 'question/checks'
8
+ require_relative 'utils'
9
+
10
+ module TTY
11
+ # A class responsible for shell prompt interactions.
12
+ class Prompt
13
+ # A class responsible for gathering user input
14
+ #
15
+ # @api public
16
+ class Question
17
+ include Checks
18
+
19
+ UndefinedSetting = Class.new do
20
+ def to_s
21
+ "undefined"
22
+ end
23
+ alias inspect to_s
24
+ end
25
+
26
+ # Store question message
27
+ # @api public
28
+ attr_reader :message
29
+
30
+ attr_reader :modifier
31
+
32
+ attr_reader :validation
33
+
34
+ # Initialize a Question
35
+ #
36
+ # @api public
37
+ def initialize(prompt, options = {})
38
+ @prompt = prompt
39
+ @prefix = options.fetch(:prefix) { @prompt.prefix }
40
+ @default = options.fetch(:default) { UndefinedSetting }
41
+ @required = options.fetch(:required) { false }
42
+ @echo = options.fetch(:echo) { true }
43
+ @in = options.fetch(:in) { UndefinedSetting }
44
+ @modifier = options.fetch(:modifier) { [] }
45
+ @validation = options.fetch(:validation) { UndefinedSetting }
46
+ @convert = options.fetch(:convert) { UndefinedSetting }
47
+ @active_color = options.fetch(:active_color) { @prompt.active_color }
48
+ @help_color = options.fetch(:help_color) { @prompt.help_color }
49
+ @error_color = options.fetch(:error_color) { :red }
50
+ @messages = Utils.deep_copy(options.fetch(:messages) { { } })
51
+ @done = false
52
+ @input = nil
53
+
54
+ @evaluator = Evaluator.new(self)
55
+
56
+ @evaluator << CheckRequired
57
+ @evaluator << CheckDefault
58
+ @evaluator << CheckRange
59
+ @evaluator << CheckValidation
60
+ @evaluator << CheckModifier
61
+ end
62
+
63
+ # Stores all the error messages displayed to user
64
+ # The currently supported messages are:
65
+ # * :range?
66
+ # * :required?
67
+ # * :valid?
68
+ attr_reader :messages
69
+
70
+ # Retrieve message based on the key
71
+ #
72
+ # @param [Symbol] name
73
+ # the name of message key
74
+ #
75
+ # @param [Hash] tokens
76
+ # the tokens to evaluate
77
+ #
78
+ # @return [Array[String]]
79
+ #
80
+ # @api private
81
+ def message_for(name, tokens = nil)
82
+ template = @messages[name]
83
+ if template && !template.match(/\%\{/).nil?
84
+ [template % tokens]
85
+ else
86
+ [template || '']
87
+ end
88
+ end
89
+
90
+ # Call the question
91
+ #
92
+ # @param [String] message
93
+ #
94
+ # @return [self]
95
+ #
96
+ # @api public
97
+ def call(message, &block)
98
+ return if Utils.blank?(message)
99
+ @message = message
100
+ block.call(self) if block
101
+ render
102
+ end
103
+
104
+ # Read answer and convert to type
105
+ #
106
+ # @api private
107
+ def render
108
+ @errors = []
109
+ until @done
110
+ question = render_question
111
+ @prompt.print(question)
112
+ result = process_input(question)
113
+ if result.failure?
114
+ @errors = result.errors
115
+ @prompt.print(render_error(result.errors))
116
+ else
117
+ @done = true
118
+ end
119
+ @prompt.print(refresh(question.lines.count))
120
+ end
121
+ @prompt.print(render_question)
122
+ convert_result(result.value)
123
+ end
124
+
125
+ # Render question
126
+ #
127
+ # @return [String]
128
+ #
129
+ # @api private
130
+ def render_question
131
+ header = "#{@prefix}#{message} "
132
+ if !echo?
133
+ header
134
+ elsif @done
135
+ header += @prompt.decorate("#{@input}", @active_color)
136
+ elsif default? && !Utils.blank?(@default)
137
+ header += @prompt.decorate("(#{default})", @help_color) + ' '
138
+ end
139
+ header << "\n" if @done
140
+ header
141
+ end
142
+
143
+ # Decide how to handle input from user
144
+ #
145
+ # @api private
146
+ def process_input(question)
147
+ @input = read_input(question)
148
+ if Utils.blank?(@input)
149
+ @input = default? ? default : nil
150
+ end
151
+ @evaluator.(@input)
152
+ end
153
+
154
+ # Process input
155
+ #
156
+ # @api private
157
+ def read_input(question)
158
+ @prompt.read_line(question, echo: echo).chomp
159
+ end
160
+
161
+ # Handle error condition
162
+ #
163
+ # @return [String]
164
+ #
165
+ # @api private
166
+ def render_error(errors)
167
+ errors.reduce('') do |acc, err|
168
+ newline = (@echo ? '' : "\n")
169
+ acc << newline + @prompt.decorate('>>', :red) + ' ' + err
170
+ acc
171
+ end
172
+ end
173
+
174
+ # Determine area of the screen to clear
175
+ #
176
+ # @param [Integer] lines
177
+ # number of lines to clear
178
+ #
179
+ # @return [String]
180
+ #
181
+ # @api private
182
+ def refresh(lines)
183
+ output = ''
184
+ if @done
185
+ if @errors.count.zero? && @echo
186
+ output << @prompt.cursor.up(lines)
187
+ else
188
+ lines += @errors.count
189
+ end
190
+ else
191
+ output << @prompt.cursor.up(lines)
192
+ end
193
+ output + @prompt.clear_lines(lines)
194
+ end
195
+
196
+ # Convert value to expected type
197
+ #
198
+ # @param [Object] value
199
+ #
200
+ # @api private
201
+ def convert_result(value)
202
+ if convert? & !Utils.blank?(value)
203
+ Converters.convert(@convert, value)
204
+ else
205
+ value
206
+ end
207
+ end
208
+
209
+ # Specify answer conversion
210
+ #
211
+ # @api public
212
+ def convert(value)
213
+ @convert = value
214
+ end
215
+
216
+ # Check if conversion is set
217
+ #
218
+ # @return [Boolean]
219
+ #
220
+ # @api public
221
+ def convert?
222
+ @convert != UndefinedSetting
223
+ end
224
+
225
+ # Set default value.
226
+ #
227
+ # @api public
228
+ def default(value = (not_set = true))
229
+ return @default if not_set
230
+ @default = value
231
+ end
232
+
233
+ # Check if default value is set
234
+ #
235
+ # @return [Boolean]
236
+ #
237
+ # @api public
238
+ def default?
239
+ @default != UndefinedSetting
240
+ end
241
+
242
+ # Ensure that passed argument is present or not
243
+ #
244
+ # @return [Boolean]
245
+ #
246
+ # @api public
247
+ def required(value = (not_set = true), message = nil)
248
+ messages[:required?] = message if message
249
+ return @required if not_set
250
+ @required = value
251
+ end
252
+ alias_method :required?, :required
253
+
254
+ # Set validation rule for an argument
255
+ #
256
+ # @param [Object] value
257
+ #
258
+ # @return [Question]
259
+ #
260
+ # @api public
261
+ def validate(value = nil, message = nil, &block)
262
+ messages[:valid?] = message if message
263
+ @validation = (value || block)
264
+ end
265
+
266
+ def validation?
267
+ @validation != UndefinedSetting
268
+ end
269
+
270
+ # Modify string according to the rule given.
271
+ #
272
+ # @param [Symbol] rule
273
+ #
274
+ # @api public
275
+ def modify(*rules)
276
+ @modifier = rules
277
+ end
278
+
279
+ # Turn terminal echo on or off. This is used to secure the display so
280
+ # that the entered characters are not echoed back to the screen.
281
+ #
282
+ # @api public
283
+ def echo(value = nil)
284
+ return @echo if value.nil?
285
+ @echo = value
286
+ end
287
+ alias_method :echo?, :echo
288
+
289
+ # Turn raw mode on or off. This enables character-based input.
290
+ #
291
+ # @api public
292
+ def raw(value = nil)
293
+ return @raw if value.nil?
294
+ @raw = value
295
+ end
296
+ alias_method :raw?, :raw
297
+
298
+ # Set expected range of values
299
+ #
300
+ # @param [String] value
301
+ #
302
+ # @api public
303
+ def in(value = (not_set = true), message = nil)
304
+ messages[:range?] = message if message
305
+ if in? && !@in.is_a?(Range)
306
+ @in = Converters.convert(:range, @in)
307
+ end
308
+ return @in if not_set
309
+ @in = Converters.convert(:range, value)
310
+ end
311
+
312
+ # Check if range is set
313
+ #
314
+ # @return [Boolean]
315
+ #
316
+ # @api public
317
+ def in?
318
+ @in != UndefinedSetting
319
+ end
320
+
321
+ # @api public
322
+ def to_s
323
+ "#{message}"
324
+ end
325
+
326
+ # String representation of this question
327
+ # @api public
328
+ def inspect
329
+ "#<#{self.class.name} @message=#{message}, @input=#{@input}>"
330
+ end
331
+ end # Question
332
+ end # Prompt
333
+ end # TTY