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