tty-prompt 0.1.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 (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