tty-prompt 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +24 -0
  6. data/Gemfile +16 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +199 -0
  9. data/Rakefile +8 -0
  10. data/lib/tty-prompt.rb +15 -0
  11. data/lib/tty/prompt.rb +206 -0
  12. data/lib/tty/prompt/distance.rb +49 -0
  13. data/lib/tty/prompt/error.rb +26 -0
  14. data/lib/tty/prompt/history.rb +16 -0
  15. data/lib/tty/prompt/mode.rb +64 -0
  16. data/lib/tty/prompt/mode/echo.rb +40 -0
  17. data/lib/tty/prompt/mode/raw.rb +40 -0
  18. data/lib/tty/prompt/question.rb +338 -0
  19. data/lib/tty/prompt/question/modifier.rb +93 -0
  20. data/lib/tty/prompt/question/validation.rb +92 -0
  21. data/lib/tty/prompt/reader.rb +113 -0
  22. data/lib/tty/prompt/response.rb +252 -0
  23. data/lib/tty/prompt/response_delegation.rb +41 -0
  24. data/lib/tty/prompt/statement.rb +60 -0
  25. data/lib/tty/prompt/suggestion.rb +113 -0
  26. data/lib/tty/prompt/utils.rb +16 -0
  27. data/lib/tty/prompt/version.rb +7 -0
  28. data/spec/spec_helper.rb +45 -0
  29. data/spec/unit/ask_spec.rb +77 -0
  30. data/spec/unit/distance/distance_spec.rb +75 -0
  31. data/spec/unit/error_spec.rb +30 -0
  32. data/spec/unit/question/argument_spec.rb +30 -0
  33. data/spec/unit/question/character_spec.rb +24 -0
  34. data/spec/unit/question/default_spec.rb +25 -0
  35. data/spec/unit/question/in_spec.rb +23 -0
  36. data/spec/unit/question/initialize_spec.rb +24 -0
  37. data/spec/unit/question/modifier/apply_to_spec.rb +31 -0
  38. data/spec/unit/question/modifier/letter_case_spec.rb +22 -0
  39. data/spec/unit/question/modifier/whitespace_spec.rb +33 -0
  40. data/spec/unit/question/modify_spec.rb +44 -0
  41. data/spec/unit/question/valid_spec.rb +46 -0
  42. data/spec/unit/question/validate_spec.rb +30 -0
  43. data/spec/unit/question/validation/coerce_spec.rb +24 -0
  44. data/spec/unit/question/validation/valid_value_spec.rb +22 -0
  45. data/spec/unit/reader/getc_spec.rb +42 -0
  46. data/spec/unit/response/read_bool_spec.rb +47 -0
  47. data/spec/unit/response/read_char_spec.rb +16 -0
  48. data/spec/unit/response/read_date_spec.rb +20 -0
  49. data/spec/unit/response/read_email_spec.rb +42 -0
  50. data/spec/unit/response/read_multiple_spec.rb +23 -0
  51. data/spec/unit/response/read_number_spec.rb +28 -0
  52. data/spec/unit/response/read_range_spec.rb +26 -0
  53. data/spec/unit/response/read_spec.rb +68 -0
  54. data/spec/unit/response/read_string_spec.rb +19 -0
  55. data/spec/unit/say_spec.rb +66 -0
  56. data/spec/unit/statement/initialize_spec.rb +19 -0
  57. data/spec/unit/suggest_spec.rb +33 -0
  58. data/spec/unit/warn_spec.rb +30 -0
  59. data/tasks/console.rake +10 -0
  60. data/tasks/coverage.rake +11 -0
  61. data/tasks/spec.rake +29 -0
  62. data/tty-prompt.gemspec +26 -0
  63. metadata +194 -0
@@ -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,26 @@
1
+ # encoding: utf-8
2
+
3
+ class Error
4
+
5
+ attr_reader :errors
6
+
7
+ def initialize(question, errors=[])
8
+ @question = question
9
+ @errors = errors
10
+ end
11
+
12
+ def <<(type, message)
13
+ @errors << [type, message]
14
+ end
15
+
16
+
17
+ # Handle exception
18
+ #
19
+ # @api private
20
+ def error_wrapping(&block)
21
+ yield
22
+ rescue
23
+ question.error? ? block.call : raise
24
+ end
25
+
26
+ end # Error
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ # A class responsible for storing shell interactions
6
+ class History
7
+
8
+ attr_reader :max_size
9
+
10
+ def initialize(max_size=nil)
11
+ @max_size = max_size
12
+ end
13
+
14
+ end # History
15
+ end # Prompt
16
+ end # TTY
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty/prompt/mode/echo'
4
+ require 'tty/prompt/mode/raw'
5
+
6
+ module TTY
7
+ class Prompt
8
+ class Mode
9
+ # Initialize a Terminal
10
+ #
11
+ # @api public
12
+ def initialize(options = {})
13
+ @echo = TTY::Prompt::Mode::Echo.new
14
+ @raw = TTY::Prompt::Mode::Raw.new
15
+ end
16
+
17
+ # Switch echo on
18
+ #
19
+ # @api public
20
+ def echo_on
21
+ @echo.on
22
+ end
23
+
24
+ # Switch echo off
25
+ #
26
+ # @api public
27
+ def echo_off
28
+ @echo.off
29
+ end
30
+
31
+ # Echo given block
32
+ #
33
+ # @param [Boolean] is_on
34
+ #
35
+ # @api public
36
+ def echo(is_on = true, &block)
37
+ @echo.echo(is_on, &block)
38
+ end
39
+
40
+ # Switch raw mode on
41
+ #
42
+ # @api public
43
+ def raw_on
44
+ @raw.on
45
+ end
46
+
47
+ # Switch raw mode off
48
+ #
49
+ # @api public
50
+ def raw_off
51
+ @raw.off
52
+ end
53
+
54
+ # Use raw mode in the given block
55
+ #
56
+ # @param [Boolean] is_on
57
+ #
58
+ # @api public
59
+ def raw(is_on = true, &block)
60
+ @raw.raw(is_on, &block)
61
+ end
62
+ end # Mode
63
+ end # Prompt
64
+ end # TTY
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ class Mode
6
+ # A class responsible for toggling echo.
7
+ class Echo
8
+ # Turn echo on
9
+ #
10
+ # @api public
11
+ def on
12
+ %x{stty echo} if TTY::Platform.unix?
13
+ end
14
+
15
+ # Turn echo off
16
+ #
17
+ # @api public
18
+ def off
19
+ %x{stty -echo} if TTY::Platform.unix?
20
+ end
21
+
22
+ # Wrap code block inside echo
23
+ #
24
+ # @api public
25
+ def echo(is_on=true, &block)
26
+ value = nil
27
+ begin
28
+ off unless is_on
29
+ value = block.call if block_given?
30
+ on
31
+ return value
32
+ rescue NoMethodError, Interrupt
33
+ on
34
+ exit
35
+ end
36
+ end
37
+ end # Echo
38
+ end # Mode
39
+ end # Prompt
40
+ end # TTY
@@ -0,0 +1,40 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ class Mode
6
+ # A class responsible for toggling raw mode.
7
+ class Raw
8
+ # Turn raw mode on
9
+ #
10
+ # @api public
11
+ def on
12
+ %x{stty raw} if TTY::Platform.unix?
13
+ end
14
+
15
+ # Turn raw mode off
16
+ #
17
+ # @api public
18
+ def off
19
+ %x{stty -raw} if TTY::Platform.unix?
20
+ end
21
+
22
+ # Wrap code block inside raw mode
23
+ #
24
+ # @api public
25
+ def raw(is_on=true, &block)
26
+ value = nil
27
+ begin
28
+ on if is_on
29
+ value = block.call if block_given?
30
+ off
31
+ return value
32
+ rescue NoMethodError, Interrupt
33
+ off
34
+ exit
35
+ end
36
+ end
37
+ end # Raw
38
+ end # Mode
39
+ end # Prompt
40
+ end # TTY
@@ -0,0 +1,338 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty/prompt/question/modifier'
4
+ require 'tty/prompt/question/validation'
5
+
6
+ require 'tty/prompt/response_delegation'
7
+
8
+ module TTY
9
+ # A class responsible for shell prompt interactions.
10
+ class Prompt
11
+ # A class representing a command line question
12
+ class Question
13
+ include ResponseDelegation
14
+
15
+ PREFIX = ' + '
16
+ MULTIPLE_PREFIX = ' * '
17
+ ERROR_PREFIX = ' ERROR:'
18
+
19
+ # Store statement.
20
+ #
21
+ # @api private
22
+ attr_accessor :statement
23
+
24
+ # Store default value.
25
+ #
26
+ # @api private
27
+ attr_reader :default_value
28
+
29
+ attr_reader :required
30
+ private :required
31
+
32
+ attr_reader :validation
33
+
34
+ # Controls character processing of the answer
35
+ #
36
+ # @api public
37
+ attr_reader :modifier
38
+
39
+ # Returns valid answers
40
+ #
41
+ # @api public
42
+ attr_reader :valid_values
43
+
44
+ attr_reader :error
45
+
46
+ # Returns character mode
47
+ #
48
+ # @api public
49
+ attr_reader :character
50
+
51
+ # @api private
52
+ attr_reader :shell
53
+ private :shell
54
+
55
+ # Initialize a Question
56
+ #
57
+ # @api public
58
+ def initialize(shell, options = {})
59
+ @shell = shell || Prompt.new
60
+ @required = options.fetch(:required) { false }
61
+ @echo = options.fetch(:echo) { true }
62
+ @raw = options.fetch(:raw) { false }
63
+ @mask = options.fetch(:mask) { false }
64
+ @character = options.fetch(:character) { false }
65
+ @in = options.fetch(:in) { false }
66
+ @modifier = Modifier.new options.fetch(:modifier) { [] }
67
+ @valid_values = options.fetch(:valid) { [] }
68
+ @validation = Validation.new options.fetch(:validation) { nil }
69
+ @default_value = nil
70
+ @error = false
71
+ @converter = Necromancer.new
72
+ end
73
+
74
+ # Set a new prompt
75
+ #
76
+ # @param [String] message
77
+ #
78
+ # @return [self]
79
+ #
80
+ # @api public
81
+ def prompt(message)
82
+ self.statement = message
83
+ shell.say shell.prefix + statement
84
+ self
85
+ end
86
+
87
+ # Set default value.
88
+ #
89
+ # @api public
90
+ def default(value)
91
+ return self if value == ''
92
+ @default_value = value
93
+ self
94
+ end
95
+
96
+ # Check if default value is set
97
+ #
98
+ # @return [Boolean]
99
+ #
100
+ # @api public
101
+ def default?
102
+ !!@default_value
103
+ end
104
+
105
+ # Ensure that passed argument is present if required option
106
+ #
107
+ # @return [Question]
108
+ #
109
+ # @api public
110
+ def argument(value)
111
+ case value
112
+ when :required
113
+ @required = true
114
+ when :optional
115
+ @required = false
116
+ end
117
+ self
118
+ end
119
+
120
+ # Check if required argument present.
121
+ #
122
+ # @return [Boolean]
123
+ #
124
+ # @api private
125
+ def required?
126
+ required
127
+ end
128
+
129
+ # Set validation rule for an argument
130
+ #
131
+ # @param [Object] value
132
+ #
133
+ # @return [Question]
134
+ #
135
+ # @api public
136
+ def validate(value = nil, &block)
137
+ @validation = Validation.new(value || block)
138
+ self
139
+ end
140
+
141
+ # Set expected values
142
+ #
143
+ # @param [Array] values
144
+ #
145
+ # @return [self]
146
+ #
147
+ # @api public
148
+ def valid(values)
149
+ @valid_values = values
150
+ self
151
+ end
152
+
153
+ # Reset question object.
154
+ #
155
+ # @api public
156
+ def clean
157
+ @statement = nil
158
+ @default_value = nil
159
+ @required = false
160
+ @modifier = nil
161
+ end
162
+
163
+ # Modify string according to the rule given.
164
+ #
165
+ # @param [Symbol] rule
166
+ #
167
+ # @api public
168
+ def modify(*rules)
169
+ @modifier = Modifier.new(*rules)
170
+ self
171
+ end
172
+
173
+ # Setup behaviour when error(s) occur
174
+ #
175
+ # @api public
176
+ def on_error(action = nil)
177
+ @error = action
178
+ self
179
+ end
180
+
181
+ # Check if error behaviour is set
182
+ #
183
+ # @api public
184
+ def error?
185
+ !!@error
186
+ end
187
+
188
+ # Turn terminal echo on or off. This is used to secure the display so
189
+ # that the entered characters are not echoed back to the screen.
190
+ #
191
+ # @api public
192
+ def echo(value = nil)
193
+ return @echo if value.nil?
194
+ @echo = value
195
+ self
196
+ end
197
+
198
+ # Chec if echo is set
199
+ #
200
+ # @api public
201
+ def echo?
202
+ !!@echo
203
+ end
204
+
205
+ # Turn raw mode on or off. This enables character-based input.
206
+ #
207
+ # @api public
208
+ def raw(value = nil)
209
+ return @raw if value.nil?
210
+ @raw = value
211
+ self
212
+ end
213
+
214
+ # Check if raw mode is set
215
+ #
216
+ # @api public
217
+ def raw?
218
+ !!@raw
219
+ end
220
+
221
+ # Set character for masking the STDIN input
222
+ #
223
+ # @param [String] character
224
+ #
225
+ # @return [self]
226
+ #
227
+ # @api public
228
+ def mask(char = nil)
229
+ return @mask if char.nil?
230
+ @mask = char
231
+ self
232
+ end
233
+
234
+ # Check if character mask is set
235
+ #
236
+ # @return [Boolean]
237
+ #
238
+ # @api public
239
+ def mask?
240
+ !!@mask
241
+ end
242
+
243
+ # Set if the input is character based or not
244
+ #
245
+ # @param [Boolean] value
246
+ #
247
+ # @return [self]
248
+ #
249
+ # @api public
250
+ def char(value = nil)
251
+ return @character if value.nil?
252
+ @character = value
253
+ self
254
+ end
255
+
256
+ # Check if character intput is set
257
+ #
258
+ # @return [Boolean]
259
+ #
260
+ # @api public
261
+ def character?
262
+ !!@character
263
+ end
264
+
265
+ # Set expect range of values
266
+ #
267
+ # @param [String] value
268
+ #
269
+ # @api public
270
+ def in(value = nil)
271
+ return @in if value.nil?
272
+ @in = @converter.convert(value).to(:range, strict: true)
273
+ self
274
+ end
275
+
276
+ # Check if range is set
277
+ #
278
+ # @return [Boolean]
279
+ #
280
+ # @api public
281
+ def in?
282
+ !!@in
283
+ end
284
+
285
+ # Check if response matches all the requirements set by the question
286
+ #
287
+ # @param [Object] value
288
+ #
289
+ # @return [Object]
290
+ #
291
+ # @api private
292
+ def evaluate_response(value)
293
+ return default_value if !value && default?
294
+ check_required(value)
295
+ return if value.nil?
296
+
297
+ check_valid(value) unless valid_values.empty?
298
+ within?(value)
299
+ validation.valid_value?(value)
300
+ modifier.apply_to(value)
301
+ end
302
+
303
+ private
304
+
305
+ # Check if value is present
306
+ #
307
+ # @api private
308
+ def check_required(value)
309
+ if required? && !default? && value.nil?
310
+ fail ArgumentRequired, 'No value provided for required'
311
+ end
312
+ end
313
+
314
+ # Check if value matches any of the expected values
315
+ #
316
+ # @api private
317
+ def check_valid(value)
318
+ if Array(value).all? { |val| valid_values.include? val }
319
+ return value
320
+ else
321
+ fail InvalidArgument, "Valid values are: #{valid_values.join(', ')}"
322
+ end
323
+ end
324
+
325
+ # Check if value is within expected range
326
+ #
327
+ # @api private
328
+ def within?(value)
329
+ if in? && value
330
+ if @in.include?(value)
331
+ else
332
+ fail InvalidArgument, "Value #{value} is not included in the range #{@in}"
333
+ end
334
+ end
335
+ end
336
+ end # Question
337
+ end # Prompt
338
+ end # TTY