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,93 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ class Question
6
+ # A class representing String modifications.
7
+ class Modifier
8
+ attr_reader :modifiers
9
+ private :modifiers
10
+
11
+ # Initialize a Modifier
12
+ #
13
+ # @api public
14
+ def initialize(*modifiers)
15
+ @modifiers = Array(modifiers)
16
+ end
17
+
18
+ # Change supplied value according to the given string transformation.
19
+ # Valid settings are:
20
+ #
21
+ # @param [String] value
22
+ # the string to be modified
23
+ #
24
+ # @return [String]
25
+ #
26
+ # @api private
27
+ def apply_to(value)
28
+ modifiers.reduce(value) do |result, mod|
29
+ result = Modifier.letter_case mod, result
30
+ Modifier.whitespace mod, result
31
+ end
32
+ end
33
+
34
+ # Changes letter casing in a string according to valid modifications.
35
+ # For invalid modification option the string is preserved.
36
+ #
37
+ # @param [Symbol] mod
38
+ # the modification to change the string
39
+ #
40
+ # @option mod [Symbol] :up change to upper case
41
+ # @option mod [Symbol] :upcase change to upper case
42
+ # @option mod [Symbol] :uppercase change to upper case
43
+ # @option mod [Symbol] :down change to lower case
44
+ # @option mod [Symbol] :downcase change to lower case
45
+ # @option mod [Symbol] :capitalize change all words to start
46
+ # with uppercase case letter
47
+ #
48
+ # @return [String]
49
+ #
50
+ # @api public
51
+ def self.letter_case(mod, value)
52
+ case mod
53
+ when :up, :upcase, :uppercase
54
+ value.upcase
55
+ when :down, :downcase, :lowercase
56
+ value.downcase
57
+ when :capitalize
58
+ value.capitalize
59
+ else
60
+ value
61
+ end
62
+ end
63
+
64
+ # Changes whitespace in a string according to valid modifications.
65
+ #
66
+ # @param [Symbol] mod
67
+ # the modification to change the string
68
+ #
69
+ # @option mod [String] :trim, :strip
70
+ # remove whitespace for the start and end
71
+ # @option mod [String] :chomp remove record separator from the end
72
+ # @option mod [String] :collapse remove any duplicate whitespace
73
+ # @option mod [String] :remove remove all whitespace
74
+ #
75
+ # @api public
76
+ def self.whitespace(mod, value)
77
+ case mod
78
+ when :trim, :strip
79
+ value.strip
80
+ when :chomp
81
+ value.chomp
82
+ when :collapse
83
+ value.gsub(/\s+/, ' ')
84
+ when :remove
85
+ value.gsub(/\s+/, '')
86
+ else
87
+ value
88
+ end
89
+ end
90
+ end # Modifier
91
+ end # Question
92
+ end # Prompt
93
+ end # TTY
@@ -0,0 +1,92 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Prompt
5
+ class Question
6
+ # A class representing question validation.
7
+ class Validation
8
+ # @api private
9
+ attr_reader :validation
10
+ private :validation
11
+
12
+ # Initialize a Validation
13
+ #
14
+ # @param [Object] validation
15
+ #
16
+ # @return [undefined]
17
+ #
18
+ # @api private
19
+ def initialize(validation = nil)
20
+ @validation = validation ? coerce(validation) : validation
21
+ end
22
+
23
+ # Convert validation into known type.
24
+ #
25
+ # @param [Object] validation
26
+ #
27
+ # @raise [TTY::ValidationCoercion] failed to convert validation
28
+ #
29
+ # @api private
30
+ def coerce(validation)
31
+ case validation
32
+ when Proc
33
+ validation
34
+ when Regexp, String
35
+ Regexp.new(validation.to_s)
36
+ else
37
+ fail ValidationCoercion, "Wrong type, got #{validation.class}"
38
+ end
39
+ end
40
+
41
+ # Check if validation is required
42
+ #
43
+ # @return [Boolean]
44
+ #
45
+ # @api public
46
+ def validate?
47
+ !!validation
48
+ end
49
+
50
+ # Test if the value matches the validation
51
+ #
52
+ # @example
53
+ # validation.valid_value?(value) # => true or false
54
+ #
55
+ # @param [Object] value
56
+ # the value to validate
57
+ #
58
+ # @return [undefined]
59
+ #
60
+ # @api public
61
+ def valid_value?(value)
62
+ check_validation(value)
63
+ end
64
+
65
+ private
66
+
67
+ # Check if provided value passes validation
68
+ #
69
+ # @param [String] value
70
+ #
71
+ # @raise [TTY::InvalidArgument] unkown type of argument
72
+ #
73
+ # @return [undefined]
74
+ #
75
+ # @api private
76
+ def check_validation(value)
77
+ if validate? && value
78
+ value = value.to_s
79
+ if validation.is_a?(Regexp) && validation =~ value
80
+ elsif validation.is_a?(Proc) && validation.call(value)
81
+ else
82
+ fail InvalidArgument, "Invalid input for #{value}"
83
+ end
84
+ true
85
+ else
86
+ false
87
+ end
88
+ end
89
+ end # Validation
90
+ end # Question
91
+ end # Prompt
92
+ end # TTY
@@ -0,0 +1,113 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ # A class responsible for shell prompt interactions.
5
+ class Prompt
6
+ # A class responsible for reading character input from STDIN
7
+ class Reader
8
+ # @api private
9
+ attr_reader :shell
10
+ private :shell
11
+
12
+ attr_reader :mode
13
+
14
+ # Key input constants for decimal codes
15
+ CARRIAGE_RETURN = 13.freeze
16
+ NEWLINE = 10.freeze
17
+ BACKSPACE = 127.freeze
18
+ DELETE = 8.freeze
19
+
20
+ # Initialize a Reader
21
+ #
22
+ # @api public
23
+ def initialize(shell = Prompt.new)
24
+ @shell = shell
25
+ @mode = Mode.new
26
+ end
27
+
28
+ # Get input in unbuffered mode.
29
+ #
30
+ # @example
31
+ # buffer do
32
+ # ...
33
+ # end
34
+ #
35
+ # @return [String]
36
+ #
37
+ # @api public
38
+ def buffer(&block)
39
+ bufferring = shell.output.sync
40
+ # Immediately flush output
41
+ shell.output.sync = true
42
+
43
+ value = block.call if block_given?
44
+
45
+ shell.output.sync = bufferring
46
+ value
47
+ end
48
+
49
+ # Get a value from STDIN one key at a time. Each key press is echoed back
50
+ # to the shell masked with character(if given). The input finishes when
51
+ # enter key is pressed.
52
+ #
53
+ # @param [String] mask
54
+ # the character to use as mask
55
+ #
56
+ # @return [String]
57
+ #
58
+ # @api public
59
+ def getc(mask = (not_set = true))
60
+ value = ''
61
+ buffer do
62
+ begin
63
+ while (char = shell.input.getbyte) &&
64
+ !(char == CARRIAGE_RETURN || char == NEWLINE)
65
+ value = handle_char value, char, not_set, mask
66
+ end
67
+ ensure
68
+ mode.echo_on
69
+ end
70
+ end
71
+ value
72
+ end
73
+
74
+ # Get a value from STDIN using line input.
75
+ #
76
+ # @api public
77
+ def gets
78
+ shell.input.gets
79
+ end
80
+
81
+ # Reads at maximum +maxlen+ characters.
82
+ #
83
+ # @param [Integer] maxlen
84
+ #
85
+ # @api public
86
+ def readpartial(maxlen)
87
+ shell.input.readpartial(maxlen)
88
+ end
89
+
90
+ private
91
+
92
+ # Handle single character by appending to or removing from output
93
+ #
94
+ # @api private
95
+ def handle_char(value, char, not_set, mask)
96
+ if char == BACKSPACE || char == DELETE
97
+ value.slice!(-1, 1) unless value.empty?
98
+ else
99
+ print_char char, not_set, mask
100
+ value << char
101
+ end
102
+ value
103
+ end
104
+
105
+ # Print out character back to shell STDOUT
106
+ #
107
+ # @api private
108
+ def print_char(char, not_set, mask)
109
+ shell.output.putc((not_set || !mask) ? char : mask)
110
+ end
111
+ end # Reader
112
+ end # Prompt
113
+ end # TTY
@@ -0,0 +1,252 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ # A class responsible for shell prompt interactions
5
+ class Prompt
6
+ # A class representing a shell response
7
+ class Response
8
+ VALID_TYPES = [
9
+ :boolean,
10
+ :string,
11
+ :symbol,
12
+ :integer,
13
+ :float,
14
+ :date,
15
+ :datetime
16
+ ]
17
+
18
+ attr_reader :reader
19
+ private :reader
20
+
21
+ attr_reader :question
22
+ private :question
23
+
24
+ # Initialize a Response
25
+ #
26
+ # @api public
27
+ def initialize(question, shell = Shell.new)
28
+ @question = question
29
+ @shell = shell
30
+ @converter = Necromancer.new
31
+ @reader = Reader.new(@shell)
32
+ end
33
+
34
+ # Read input from STDIN either character or line
35
+ #
36
+ # @param [Symbol] type
37
+ #
38
+ # @return [undefined]
39
+ #
40
+ # @api public
41
+ def read(type = nil)
42
+ question.evaluate_response read_input
43
+ end
44
+
45
+ # @api private
46
+ def read_input
47
+ if question.mask? && question.echo?
48
+ reader.getc(question.mask)
49
+ else
50
+ reader.mode.echo(question.echo) do
51
+ reader.mode.raw(question.raw) do
52
+ if question.raw?
53
+ reader.readpartial(10)
54
+ elsif question.character?
55
+ reader.getc(question.mask)
56
+ else
57
+ reader.gets
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def no_input?(input)
65
+ !input || input == "\n" || input.empty?
66
+ end
67
+
68
+ # @api private
69
+ def evaluate_response
70
+ input = read_input
71
+ input = if no_input?(input)
72
+ nil
73
+ elsif block_given?
74
+ yield(input)
75
+ else input
76
+ end
77
+ question.evaluate_response(input)
78
+ end
79
+
80
+ # Read answer and cast to String type
81
+ #
82
+ # @param [String] error
83
+ # error to display on failed conversion to string type
84
+ #
85
+ # @api public
86
+ def read_string(error = nil)
87
+ evaluate_response { |input| String(input).strip }
88
+ end
89
+
90
+ # Read answer's first character
91
+ #
92
+ # @api public
93
+ def read_char
94
+ question.char(true)
95
+ evaluate_response { |input| String(input).chars.to_a[0] }
96
+ end
97
+
98
+ # Read multiple line answer and cast to String type
99
+ #
100
+ # @api public
101
+ def read_text
102
+ evaluate_response { |input| String(input) }
103
+ end
104
+
105
+ # Read ansewr and cast to Symbol type
106
+ #
107
+ # @api public
108
+ def read_symbol(error = nil)
109
+ evaluate_response { |input| input.to_sym }
110
+ end
111
+
112
+ # Read answer from predifined choicse
113
+ #
114
+ # @api public
115
+ def read_choice(type = nil)
116
+ question.argument(:required) unless question.default?
117
+ evaluate_response
118
+ end
119
+
120
+ # Read integer value
121
+ #
122
+ # @api public
123
+ def read_int(error = nil)
124
+ evaluate_response { |input| @converter.convert(input).to(:integer) }
125
+ end
126
+
127
+ # Read float value
128
+ #
129
+ # @api public
130
+ def read_float(error = nil)
131
+ evaluate_response { |input| @converter.convert(input).to(:float) }
132
+ end
133
+
134
+ # Read regular expression
135
+ #
136
+ # @api public
137
+ def read_regex(error = nil)
138
+ evaluate_response { |input| Kernel.send(:Regex, input) }
139
+ end
140
+
141
+ # Read range expression
142
+ #
143
+ # @api public
144
+ def read_range
145
+ evaluate_response { |input| @converter.convert(input).to(:range, strict: true) }
146
+ end
147
+
148
+ # Read date
149
+ #
150
+ # @api public
151
+ def read_date
152
+ evaluate_response { |input| @converter.convert(input).to(:date) }
153
+ end
154
+
155
+ # Read datetime
156
+ #
157
+ # @api public
158
+ def read_datetime
159
+ evaluate_response { |input| @converter.convert(input).to(:datetime) }
160
+ end
161
+
162
+ # Read boolean
163
+ #
164
+ # @api public
165
+ def read_bool(error = nil)
166
+ evaluate_response { |input| @converter.convert(input).to(:boolean, strict: true) }
167
+ end
168
+
169
+ # Read file contents
170
+ #
171
+ # @api public
172
+ def read_file(error = nil)
173
+ evaluate_response { |input| File.open(File.join(directory, input)) }
174
+ end
175
+
176
+ # Read string answer and validate against email regex
177
+ #
178
+ # @return [String]
179
+ #
180
+ # @api public
181
+ def read_email
182
+ question.validate(/^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i)
183
+ question.prompt(question.statement) if question.error
184
+ with_exception { read_string }
185
+ end
186
+
187
+ # Read answer provided on multiple lines
188
+ #
189
+ # @api public
190
+ def read_multiple
191
+ response = ''
192
+ loop do
193
+ value = evaluate_response
194
+ break if !value || value == ''
195
+ next if value !~ /\S/
196
+ response << value
197
+ end
198
+ response
199
+ end
200
+
201
+ # Read password
202
+ #
203
+ # @api public
204
+ def read_password
205
+ question.echo false
206
+ evaluate_response
207
+ end
208
+
209
+ # Read a single keypress
210
+ #
211
+ # @api public
212
+ def read_keypress
213
+ question.echo false
214
+ question.raw true
215
+ question.evaluate_response(read_input).tap do |key|
216
+ raise Interrupt if key == "\x03" # Ctrl-C
217
+ end
218
+ end
219
+
220
+ private
221
+
222
+ # Ignore exception
223
+ #
224
+ # @api private
225
+ def with_exception(&block)
226
+ yield
227
+ rescue
228
+ question.error? ? block.call : raise
229
+ end
230
+
231
+ # @param [Symbol] type
232
+ # :boolean, :string, :numeric, :array
233
+ #
234
+ # @api private
235
+ def read_type(class_or_name)
236
+ raise TypeError, "Type #{type} is not valid" if type && !valid_type?(type)
237
+ case type
238
+ when :string, ::String
239
+ read_string
240
+ when :symbol, ::Symbol
241
+ read_symbol
242
+ when :float, ::Float
243
+ read_float
244
+ end
245
+ end
246
+
247
+ def valid_type?(type)
248
+ self.class::VALID_TYPES.include? type.to_sym
249
+ end
250
+ end # Response
251
+ end # Prompt
252
+ end # TTY