tty-prompt 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -2
  3. data/CHANGELOG.md +12 -0
  4. data/README.md +223 -59
  5. data/lib/tty/prompt/choice.rb +83 -0
  6. data/lib/tty/prompt/choices.rb +92 -0
  7. data/lib/tty/prompt/codes.rb +32 -0
  8. data/lib/tty/prompt/cursor.rb +131 -0
  9. data/lib/tty/prompt/list.rb +209 -0
  10. data/lib/tty/prompt/mode/echo.rb +10 -9
  11. data/lib/tty/prompt/mode/raw.rb +10 -9
  12. data/lib/tty/prompt/multi_list.rb +105 -0
  13. data/lib/tty/prompt/question/validation.rb +12 -27
  14. data/lib/tty/prompt/question.rb +58 -107
  15. data/lib/tty/prompt/reader.rb +44 -11
  16. data/lib/tty/prompt/response.rb +31 -36
  17. data/lib/tty/prompt/response_delegation.rb +3 -2
  18. data/lib/tty/prompt/statement.rb +10 -10
  19. data/lib/tty/prompt/test.rb +15 -0
  20. data/lib/tty/prompt/version.rb +3 -3
  21. data/lib/tty/prompt.rb +72 -9
  22. data/lib/tty-prompt.rb +11 -0
  23. data/spec/unit/ask_spec.rb +32 -35
  24. data/spec/unit/choice/eql_spec.rb +24 -0
  25. data/spec/unit/choice/from_spec.rb +25 -0
  26. data/spec/unit/choices/add_spec.rb +14 -0
  27. data/spec/unit/choices/each_spec.rb +15 -0
  28. data/spec/unit/choices/new_spec.rb +12 -0
  29. data/spec/unit/choices/pluck_spec.rb +11 -0
  30. data/spec/unit/cursor/new_spec.rb +74 -0
  31. data/spec/unit/error_spec.rb +4 -8
  32. data/spec/unit/multi_select_spec.rb +163 -0
  33. data/spec/unit/question/character_spec.rb +5 -16
  34. data/spec/unit/question/default_spec.rb +4 -10
  35. data/spec/unit/question/in_spec.rb +15 -12
  36. data/spec/unit/question/initialize_spec.rb +1 -6
  37. data/spec/unit/question/modify_spec.rb +25 -24
  38. data/spec/unit/question/required_spec.rb +31 -0
  39. data/spec/unit/question/validate_spec.rb +25 -17
  40. data/spec/unit/question/validation/call_spec.rb +22 -0
  41. data/spec/unit/response/read_bool_spec.rb +38 -27
  42. data/spec/unit/response/read_char_spec.rb +5 -8
  43. data/spec/unit/response/read_date_spec.rb +8 -12
  44. data/spec/unit/response/read_email_spec.rb +25 -22
  45. data/spec/unit/response/read_multiple_spec.rb +11 -13
  46. data/spec/unit/response/read_number_spec.rb +12 -16
  47. data/spec/unit/response/read_range_spec.rb +10 -13
  48. data/spec/unit/response/read_spec.rb +39 -38
  49. data/spec/unit/response/read_string_spec.rb +7 -12
  50. data/spec/unit/say_spec.rb +10 -14
  51. data/spec/unit/select_spec.rb +192 -0
  52. data/spec/unit/statement/initialize_spec.rb +0 -4
  53. data/spec/unit/suggest_spec.rb +6 -9
  54. data/spec/unit/warn_spec.rb +4 -8
  55. metadata +32 -8
  56. data/spec/unit/question/argument_spec.rb +0 -30
  57. data/spec/unit/question/valid_spec.rb +0 -46
  58. data/spec/unit/question/validation/valid_value_spec.rb +0 -22
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'tty/prompt/question/modifier'
4
4
  require 'tty/prompt/question/validation'
5
-
6
5
  require 'tty/prompt/response_delegation'
7
6
 
8
7
  module TTY
@@ -12,23 +11,15 @@ module TTY
12
11
  class Question
13
12
  include ResponseDelegation
14
13
 
15
- PREFIX = ' + '
16
- MULTIPLE_PREFIX = ' * '
17
- ERROR_PREFIX = ' ERROR:'
18
-
19
- # Store statement.
20
- #
21
- # @api private
22
- attr_accessor :statement
14
+ # Store question message
15
+ # @api public
16
+ attr_reader :message
23
17
 
24
18
  # Store default value.
25
19
  #
26
20
  # @api private
27
21
  attr_reader :default_value
28
22
 
29
- attr_reader :required
30
- private :required
31
-
32
23
  attr_reader :validation
33
24
 
34
25
  # Controls character processing of the answer
@@ -36,11 +27,6 @@ module TTY
36
27
  # @api public
37
28
  attr_reader :modifier
38
29
 
39
- # Returns valid answers
40
- #
41
- # @api public
42
- attr_reader :valid_values
43
-
44
30
  attr_reader :error
45
31
 
46
32
  # Returns character mode
@@ -49,14 +35,13 @@ module TTY
49
35
  attr_reader :character
50
36
 
51
37
  # @api private
52
- attr_reader :shell
53
- private :shell
38
+ attr_reader :prompt
54
39
 
55
40
  # Initialize a Question
56
41
  #
57
42
  # @api public
58
- def initialize(shell, options = {})
59
- @shell = shell || Prompt.new
43
+ def initialize(prompt, options = {})
44
+ @prompt = prompt || Prompt.new
60
45
  @required = options.fetch(:required) { false }
61
46
  @echo = options.fetch(:echo) { true }
62
47
  @raw = options.fetch(:raw) { false }
@@ -64,33 +49,40 @@ module TTY
64
49
  @character = options.fetch(:character) { false }
65
50
  @in = options.fetch(:in) { false }
66
51
  @modifier = Modifier.new options.fetch(:modifier) { [] }
67
- @valid_values = options.fetch(:valid) { [] }
68
- @validation = Validation.new options.fetch(:validation) { nil }
69
- @default_value = nil
52
+ @validation = Validation.new(options.fetch(:validation) { nil })
53
+ @default = options.fetch(:default) { nil }
70
54
  @error = false
71
55
  @converter = Necromancer.new
56
+ @read = options.fetch(:read) { nil }
72
57
  end
73
58
 
74
- # Set a new prompt
59
+ # Call the quesiton
75
60
  #
76
61
  # @param [String] message
77
62
  #
78
63
  # @return [self]
79
64
  #
80
65
  # @api public
81
- def prompt(message)
82
- self.statement = message
83
- shell.say shell.prefix + statement
84
- self
66
+ def call(message, &block)
67
+ @message = message
68
+ block.call(self) if block
69
+ prompt.output.print("#{prompt.prefix}#{message}")
70
+ render
71
+ end
72
+
73
+ # Reader answer and convert to type
74
+ #
75
+ # @api private
76
+ def render
77
+ dispatch.read_type(@read)
85
78
  end
86
79
 
87
80
  # Set default value.
88
81
  #
89
82
  # @api public
90
83
  def default(value)
91
- return self if value == ''
92
- @default_value = value
93
- self
84
+ return @default unless value
85
+ @default = value
94
86
  end
95
87
 
96
88
  # Check if default value is set
@@ -99,31 +91,16 @@ module TTY
99
91
  #
100
92
  # @api public
101
93
  def default?
102
- !!@default_value
94
+ !!@default
103
95
  end
104
96
 
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.
97
+ # Ensure that passed argument is present or not
121
98
  #
122
99
  # @return [Boolean]
123
100
  #
124
- # @api private
125
- def required?
126
- required
101
+ # @api public
102
+ def required(value)
103
+ @required = value
127
104
  end
128
105
 
129
106
  # Set validation rule for an argument
@@ -135,29 +112,6 @@ module TTY
135
112
  # @api public
136
113
  def validate(value = nil, &block)
137
114
  @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
115
  end
162
116
 
163
117
  # Modify string according to the rule given.
@@ -167,7 +121,6 @@ module TTY
167
121
  # @api public
168
122
  def modify(*rules)
169
123
  @modifier = Modifier.new(*rules)
170
- self
171
124
  end
172
125
 
173
126
  # Setup behaviour when error(s) occur
@@ -175,7 +128,6 @@ module TTY
175
128
  # @api public
176
129
  def on_error(action = nil)
177
130
  @error = action
178
- self
179
131
  end
180
132
 
181
133
  # Check if error behaviour is set
@@ -192,7 +144,6 @@ module TTY
192
144
  def echo(value = nil)
193
145
  return @echo if value.nil?
194
146
  @echo = value
195
- self
196
147
  end
197
148
 
198
149
  # Chec if echo is set
@@ -208,7 +159,6 @@ module TTY
208
159
  def raw(value = nil)
209
160
  return @raw if value.nil?
210
161
  @raw = value
211
- self
212
162
  end
213
163
 
214
164
  # Check if raw mode is set
@@ -228,7 +178,6 @@ module TTY
228
178
  def mask(char = nil)
229
179
  return @mask if char.nil?
230
180
  @mask = char
231
- self
232
181
  end
233
182
 
234
183
  # Check if character mask is set
@@ -250,7 +199,6 @@ module TTY
250
199
  def char(value = nil)
251
200
  return @character if value.nil?
252
201
  @character = value
253
- self
254
202
  end
255
203
 
256
204
  # Check if character intput is set
@@ -270,7 +218,6 @@ module TTY
270
218
  def in(value = nil)
271
219
  return @in if value.nil?
272
220
  @in = @converter.convert(value).to(:range, strict: true)
273
- self
274
221
  end
275
222
 
276
223
  # Check if range is set
@@ -289,15 +236,32 @@ module TTY
289
236
  # @return [Object]
290
237
  #
291
238
  # @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)
239
+ def evaluate_response(input)
240
+ return @default if !input && default?
241
+ check_required(input)
242
+ return if input.nil?
243
+
244
+ within?(input)
245
+ validation.(input)
246
+ modifier.apply_to(input)
247
+ end
248
+
249
+ # Reset question object.
250
+ #
251
+ # @api public
252
+ def clean
253
+ @message = nil
254
+ @default = nil
255
+ @required = false
256
+ @modifier = nil
257
+ end
258
+
259
+ def to_s
260
+ "#{message}"
261
+ end
262
+
263
+ def inspect
264
+ "#<Question @message=#{message}>"
301
265
  end
302
266
 
303
267
  private
@@ -306,31 +270,18 @@ module TTY
306
270
  #
307
271
  # @api private
308
272
  def check_required(value)
309
- if required? && !default? && value.nil?
273
+ if @required && !default? && value.nil?
310
274
  fail ArgumentRequired, 'No value provided for required'
311
275
  end
312
276
  end
313
277
 
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
278
  # Check if value is within expected range
326
279
  #
327
280
  # @api private
328
281
  def within?(value)
329
282
  if in? && value
330
- if @in.include?(value)
331
- else
332
- fail InvalidArgument, "Value #{value} is not included in the range #{@in}"
333
- end
283
+ @in.include?(value) || fail(InvalidArgument,
284
+ "Value #{value} is not included in the range #{@in}")
334
285
  end
335
286
  end
336
287
  end # Question
@@ -6,8 +6,8 @@ module TTY
6
6
  # A class responsible for reading character input from STDIN
7
7
  class Reader
8
8
  # @api private
9
- attr_reader :shell
10
- private :shell
9
+ attr_reader :prompt
10
+ private :prompt
11
11
 
12
12
  attr_reader :mode
13
13
 
@@ -20,8 +20,8 @@ module TTY
20
20
  # Initialize a Reader
21
21
  #
22
22
  # @api public
23
- def initialize(shell = Prompt.new)
24
- @shell = shell
23
+ def initialize(prompt = Prompt.new)
24
+ @prompt = prompt
25
25
  @mode = Mode.new
26
26
  end
27
27
 
@@ -36,16 +36,49 @@ module TTY
36
36
  #
37
37
  # @api public
38
38
  def buffer(&block)
39
- bufferring = shell.output.sync
39
+ bufferring = prompt.output.sync
40
40
  # Immediately flush output
41
- shell.output.sync = true
41
+ prompt.output.sync = true
42
42
 
43
43
  value = block.call if block_given?
44
44
 
45
- shell.output.sync = bufferring
45
+ prompt.output.sync = bufferring
46
46
  value
47
47
  end
48
48
 
49
+ # Read a single keypress that may include
50
+ # 2 or 3 escape characters.
51
+ #
52
+ # @return [String]
53
+ #
54
+ # @api public
55
+ def read_keypress
56
+ buffer do
57
+ mode.echo(false) do
58
+ mode.raw(true) do
59
+ read_char
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ # Reads single character including invisible multibyte codes
66
+ #
67
+ # @return [String]
68
+ #
69
+ # @api public
70
+ def read_char
71
+ chars = prompt.input.getc.chr
72
+ if chars == "\e"
73
+ chars = prompt.input.read_nonblock(3) rescue chars
74
+ chars = prompt.input.read_nonblock(2) rescue chars
75
+ chars = "\e" + chars
76
+ end
77
+ chars
78
+ rescue
79
+ chars
80
+ end
81
+
49
82
  # Get a value from STDIN one key at a time. Each key press is echoed back
50
83
  # to the shell masked with character(if given). The input finishes when
51
84
  # enter key is pressed.
@@ -60,7 +93,7 @@ module TTY
60
93
  value = ''
61
94
  buffer do
62
95
  begin
63
- while (char = shell.input.getbyte) &&
96
+ while (char = prompt.input.getbyte) &&
64
97
  !(char == CARRIAGE_RETURN || char == NEWLINE)
65
98
  value = handle_char value, char, not_set, mask
66
99
  end
@@ -75,7 +108,7 @@ module TTY
75
108
  #
76
109
  # @api public
77
110
  def gets
78
- shell.input.gets
111
+ prompt.input.gets
79
112
  end
80
113
 
81
114
  # Reads at maximum +maxlen+ characters.
@@ -84,7 +117,7 @@ module TTY
84
117
  #
85
118
  # @api public
86
119
  def readpartial(maxlen)
87
- shell.input.readpartial(maxlen)
120
+ prompt.input.readpartial(maxlen)
88
121
  end
89
122
 
90
123
  private
@@ -106,7 +139,7 @@ module TTY
106
139
  #
107
140
  # @api private
108
141
  def print_char(char, not_set, mask)
109
- shell.output.putc((not_set || !mask) ? char : mask)
142
+ prompt.output.putc((not_set || !mask) ? char : mask)
110
143
  end
111
144
  end # Reader
112
145
  end # Prompt
@@ -5,16 +5,6 @@ module TTY
5
5
  class Prompt
6
6
  # A class representing a shell response
7
7
  class Response
8
- VALID_TYPES = [
9
- :boolean,
10
- :string,
11
- :symbol,
12
- :integer,
13
- :float,
14
- :date,
15
- :datetime
16
- ]
17
-
18
8
  attr_reader :reader
19
9
  private :reader
20
10
 
@@ -24,11 +14,11 @@ module TTY
24
14
  # Initialize a Response
25
15
  #
26
16
  # @api public
27
- def initialize(question, shell = Shell.new)
17
+ def initialize(question, prompt)
28
18
  @question = question
29
- @shell = shell
19
+ @prompt = prompt
30
20
  @converter = Necromancer.new
31
- @reader = Reader.new(@shell)
21
+ @reader = Reader.new(@prompt)
32
22
  end
33
23
 
34
24
  # Read input from STDIN either character or line
@@ -109,14 +99,6 @@ module TTY
109
99
  evaluate_response { |input| input.to_sym }
110
100
  end
111
101
 
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
102
  # Read integer value
121
103
  #
122
104
  # @api public
@@ -180,14 +162,14 @@ module TTY
180
162
  # @api public
181
163
  def read_email
182
164
  question.validate(/^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i)
183
- question.prompt(question.statement) if question.error
165
+ question.call("\n" + question.statement) if question.error?
184
166
  with_exception { read_string }
185
167
  end
186
168
 
187
169
  # Read answer provided on multiple lines
188
170
  #
189
171
  # @api public
190
- def read_multiple
172
+ def read_multiline
191
173
  response = ''
192
174
  loop do
193
175
  value = evaluate_response
@@ -217,8 +199,6 @@ module TTY
217
199
  end
218
200
  end
219
201
 
220
- private
221
-
222
202
  # Ignore exception
223
203
  #
224
204
  # @api private
@@ -232,21 +212,36 @@ module TTY
232
212
  # :boolean, :string, :numeric, :array
233
213
  #
234
214
  # @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
215
+ def read_type(class_or_name = nil)
216
+ case class_or_name
217
+ when :bool
218
+ read_bool
219
+ when :email
220
+ read_email
221
+ when :char
222
+ read_char
223
+ when :date
224
+ read_date
225
+ when :int
226
+ read_int
227
+ when :range
228
+ read_range
229
+ when :multiline
230
+ read_multiline
231
+ when :float
232
+ read_float
233
+ when :file
234
+ read_file
235
+ when :string
239
236
  read_string
240
- when :symbol, ::Symbol
237
+ when :symbol
241
238
  read_symbol
242
- when :float, ::Float
243
- read_float
239
+ when :keypress
240
+ read_keypress
241
+ else
242
+ read
244
243
  end
245
244
  end
246
-
247
- def valid_type?(type)
248
- self.class::VALID_TYPES.include? type.to_sym
249
- end
250
245
  end # Response
251
246
  end # Prompt
252
247
  end # TTY
@@ -25,14 +25,15 @@ module TTY
25
25
  :read_regex,
26
26
  :read_string,
27
27
  :read_symbol,
28
- :read_text
28
+ :read_text,
29
+ :read_type
29
30
 
30
31
  # Create response instance when question readed is invoked
31
32
  #
32
33
  # @param [Response] response
33
34
  #
34
35
  # @api private
35
- def dispatch(response = Response.new(self, shell))
36
+ def dispatch(response = Response.new(self, prompt))
36
37
  @response ||= response
37
38
  end
38
39
 
@@ -3,11 +3,11 @@
3
3
  module TTY
4
4
  # A class responsible for shell prompt interactions.
5
5
  class Prompt
6
- # A class representing a statement output to shell.
6
+ # A class representing a statement output to prompt.
7
7
  class Statement
8
8
  # @api private
9
- attr_reader :shell
10
- private :shell
9
+ attr_reader :prompt
10
+ private :prompt
11
11
 
12
12
  # Flag to display newline
13
13
  #
@@ -21,7 +21,7 @@ module TTY
21
21
 
22
22
  # Initialize a Statement
23
23
  #
24
- # @param [TTY::Shell] shell
24
+ # @param [TTY::Prompt] prompt
25
25
  #
26
26
  # @param [Hash] options
27
27
  #
@@ -32,14 +32,14 @@ module TTY
32
32
  # change the message display to color
33
33
  #
34
34
  # @api public
35
- def initialize(shell = Prompt.new, options = {})
36
- @shell = shell
35
+ def initialize(prompt = Prompt.new, options = {})
36
+ @prompt = prompt
37
37
  @pastel = Pastel.new
38
38
  @newline = options.fetch(:newline, true)
39
39
  @color = options.fetch(:color, false)
40
40
  end
41
41
 
42
- # Output the message to the shell
42
+ # Output the message to the prompt
43
43
  #
44
44
  # @param [String] message
45
45
  # the message to be printed to stdout
@@ -49,10 +49,10 @@ module TTY
49
49
  message = @pastel.decorate message, *color if color
50
50
 
51
51
  if newline && /( |\t)(\e\[\d+(;\d+)*m)?\Z/ !~ message
52
- shell.output.puts message
52
+ prompt.output.puts message
53
53
  else
54
- shell.output.print message
55
- shell.output.flush
54
+ prompt.output.print message
55
+ prompt.output.flush
56
56
  end
57
57
  end
58
58
  end # Statement
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty/prompt'
4
+
5
+ module TTY
6
+ # Used for initializing test cases
7
+ class TestPrompt < Prompt
8
+ def initialize(options = {})
9
+ @input = StringIO.new
10
+ @output = StringIO.new
11
+
12
+ super(@input, @output, options)
13
+ end
14
+ end # TestPrompt
15
+ end # TTY
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TTY
4
4
  class Prompt
5
- VERSION = "0.1.0"
6
- end
7
- end
5
+ VERSION = "0.2.0"
6
+ end # Prompt
7
+ end # TTY