tty-prompt 0.1.0 → 0.2.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 (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